关键字:c++预定义的单词
标识符:程序员声明的单词,它命名程序正文中的一些实体
文字:在程序中直接使用符号表示的数据
操作符:用于实现各种运算的符号
分隔符:(){},:;
空白符:空格、制表符、垂直制表符、换行符、回车符、注释
alignas alignof asm auto bool break case catchchar char16_t char32_t
class const constexpr const_cast continue decltype default delete do
double dynamic_cast else enum explicit export extern false float for
friend goto if inline int long mutable namespace new noexcept nullptr
operator private proteceted public reinterpret_cast return short signed
sizeof static static_cast struct switch template this thread_local throw
true try typedef typeid typename union unsigned using virtual void volatile
wchar_t while
基本数据类型:整数、浮点数、字符、布尔
程序中的数据:
常量:不可改变值的量
变量:允许改变的量
文字常量:如1,2,‘a’等不可改变的
int a = 0;
int a(0);
int a = {0};
int a{0};
const 加常量值
constexpr 加常量表达式
typedef int Nautural
using area = double;
分别表示Natural
为int
的别名,area
为double
的别名
头文件中含有c++自带的一些函数功能,这些函数实用但实现难度高,包含在c++自带头文件中
同时,也可将自写的".h"文件引用到程序中,作为头文件
#include //万能头
#include //标准输入输出库
#include //数学库
#include //输入输出流
#include //算法库
#include"marx"//自写库
命名空间的意义在于区分重名变量、函数,实现封装
namespace Segtree{
query();
update();
...
}
若想使用其中的函数或者变量,需要声明作用域,或用using
语句
std::query();
using namespace Segtree;
在计算机中,我们一般通过二进制存储信息
位(bit),即比特,用“b“来表示,表示一位二进制
字节(byte),用”B“来表示,是计算机中数据处理的基本单位,规定一个字节由八个二进制位构成
在计算机中,正数的二进制最高位为0,负数则为1,以下原码、反码、补码的变换中均不涉及最高位的变化
对于正数来说,原码、反码、补码相同,都为其二进制本身
对于负数来说:
原码:二进制位本身
反码:按位取反
补码:反码+1
如此,在实现负数运算时,便可用加法实现
如在最高位为第四位的基础下:6,-6的原码补码分别为:
6的原码、反码、补码:0110
-6的原码:1110 反码:1001 补码:1010
而6的原码和-6的反码相加后等于10000,由于最高位是第四位,第五位的1被舍去,即为0
c++中的数据类型分为两种:预定义类型和自定义数据类型
预定义类型:整型、字符型、布尔型、浮点型、空类型、指针类型
自定义类型:数组、结构体、联合体、枚举
类型修饰符:signed
,unsigned
,short
,long
整型:
short
(短整型),2字节;
int
(整型),4字节;
long
(长整型),4字节
long long
(长长整型),8字节
整型用来表示一个整数
浮点型:
float
(单精度浮点型),占4字节
double
(双精度浮点型),占8字节
long double
(长精度浮点型),占8字节(有些编译器中占用10、12字节)
字符型:
char
(字符型),占1字节与ASCII码一一对应
布尔类型:
bool
(逻辑性变量的定义符),占1字节在输入输出函数中,我们可能需要用到占位符,会在稍后讲到
占位符 | 含义 |
---|---|
%d | int |
%ld | long |
%f | float |
%lf | double |
%p | 地址 |
%x/%X | 十六进制 |
%o | 八进制 |
%s | 字符串 |
%u | 无符号十进制整数 |
%e/%E | 科学计数法 |
auto、delctype、enum、union、struct
auto:可自动判断变量的类型
delctype:可以定义与某一变量相同类型的变量
decltype(i) j = 5;
enum:枚举类型,定义格式为
enum<类型名>{<枚举常量表>};
enum week{Sun,Mon,Tue,Wed,Thu,Fri,Sat};
对于上述 w e e k week week枚举, S u n = 0 Sun=0 Sun=0,且此后依次递增
enum week{Sun = 7,Mon = 1,Tue,Wed,Thu,Fri,Sat};
此时, T u e Tue Tue从2开始依次递增
枚举成员只能以标识符形式,不能是常量
枚举可增加程序的可读性,也可以用来描述状态量
例如:
enum weekday{mon,tue,wed,thur,fri,sat,sun};
enum weekday day;
day = mon;
//等价于int day = 0;
通常,枚举类型和 s w i t c h switch switch等语句一起使用
union:联合体类型
联合体中有多个成员,且对一个成员的修改可影响到其他成员
union ascii{
char ch;
int number;
}
ascii a;
a.ch = 'A';
//此时,a.number = 65
a.number = 66
//此时,a.ch = 'B'
同时,一个联合体所占的内存为成员字节数最大值
struct:结构体类型
结构体中可定义其他数据类型,以及函数
在c++中,class可替代结构体,将在后续详细讲解
+ - * / (若整数相除,结果取整)%(取余)
优先级和结合性
先乘除,后加减,同级自左至右
++,–(自增,自减)
赋值符号=
,将右侧的值赋给左侧
n = n + 5;
即 n n n增大5
格式
表达式1,表达式2
求解顺序及结构
先求表达式1,再求解表达式2,最终结果为表达式2的值
例
a = 3*5 , a*4;
最终结果为60
< <= > >=
高优先级 == !=
低优先级
bool
,值只能为true
或false
逻辑运算符
!
(非) &&
(与)||
(或)
优先级由高到低
逻辑表达式
如(a>b)&&(x>y)
其结果类型为bool
格式
表达式1?表达式2:表达式3
执行顺序:
例子: x = a > b ? a : b ;
&
|
^
~
<< >>
在c++中,将数据从一个对象到另一个对象的流动抽象为“流” 流在使用前要被建立,使用后要被删除
从流中获取数据的操作称为提取操作,向流中添加数据的操作称为插入操作
数据的输入与输出是通过I/O流来实现的,cin和cout是预定于的流类对象 cin用来处理标准输入,即键盘输入,cout用来处理标准输出,即屏幕输出
<<是预定义的插入符,作用在流类对象cout上便可实现标准输出设备输出,标准输入是将提取符>>作用在流类对象cin上
int x;
cin >> x;//键盘输入x
cout << x << endl;//屏幕输出x的值,并换行
cout << "x" << ends;//屏幕输出x字符,并空格
而对于cout,有许多流操作符,可控制输出格式
操作符名 | 含义 |
---|---|
dec |
以十进制表示 |
hex |
以十六进制表示 |
oct |
以八进制表示 |
ws |
提取空白符 |
endl |
插入换行符,并刷新流 |
ends |
插入空字符 |
setprecision(int) |
设置浮点数的小数位数 |
set(int) |
设置域宽 |
getchar()
,putchar()
,可读取、输出一个字符
gets()
,puts()
,可读取、输出一个字符串,读到空格或者换行时终止
print()
,括号内引号中写输出的字符串及变量,若想输出变量,则在输出的位置用占位符代替,并在引号外按顺序注明变量
scanf()
,括号内引号中写入占位符,与输入变量一致,引号外注明变量地址
int x;
scanf("%d",&x);
printf("%d",x);
在输出中,我们可以用转义字符来控制格式,转义字符以反斜线"\"开头,后跟一个或几个字符。转义字符具有特定的含义,不同于字符原有的意义,故称“转义”字符
转义符 | 字符名 |
---|---|
’ | 单引号 |
" | 双引号 |
\ | 反斜杠 |
\0 | 空格 |
\a | 感叹号 |
\b | 退格 |
\f | 换页 |
\n | 换行 |
\r | 回车 |
\t | 水平tab |
\v | 垂直tab |
printf("\",\\,\0,\n1");
//"\
//1
程序的执行流程不总是顺序的,因此
if
语句if(表达式){
}else if(表达式){
}else{
}
else if
中的表达式是否成立else
中的程序if
,else if
和else
不必需false
,则会被转换成true
switch
语句switch(表达式){
case 常量表达式:语句
break;
case 常量表达式:语句
break;
default:语句
}
case
并执行语句default
处while
语句while(表达式){
语句
}
do while
语句do{
语句
}while(表达式)
while
一样for
语句for(语句1;表达式;语句2){
语句3
}
先执行语句1
,再判断表达式
,再执行语句3
,再执行语句2
,之后除语句1
外循环执行
同时,c++允许将迭代器遍历容器简化成范围循环:专门针对容器类型的for
循环,使用冒号``:```语句来迭代容器中的每一个元素
int a[] = {1,2,3};
for(const auto &it:a){
cout << it << ends;
}
goto
语句goto 标识符;
语句
标识符:
标识符
时,跳转至标识符:
处goto
在程序设计中,我们可能会多次使用一些功能,为了在减少重复代码的同时增加程序的泛用性,我们引入函数
声明
类型标识符 函数名(形式参数表);
定义
类型标识符 函数名(形式参数表){
语句
}
函数名(实际参数表);
根据函数的类型不同,调用函数可有不同作用
void
无返回值类型函数int
等其他数据类型函数,可返回同样数据类型的对象,可作为右值在函数定义中的为形式参数
在程序运行中定义的,为实际参数
在一般情况下,函数中对形式参数的值修改不会影响到实际参数,即单向传递,如下
void swap(int x,int y){
int t = x;
x = y;
y = t;
}
该函数不会真正交换x
,y
的值,若想改变则需要传引用或传指针,稍后再详细介绍
形参被调用后才被创建
默认参数
我们可以在定义函数的时候定义参数的默认值
int Add(int x = 0,int y = 1){
return x + y;
}
int Z = Add();
冒号表达式可以进行函数的赋值语句,多用于类的构造函数中常量的赋值
class Point{
Point(int x,int y,int m);
private:
int x,y;
const int m;
}
Point::Point(int x,int y,int m):x(x),y(y),m(m){}
关于构造函数的详细知识将在讲解类时详细说明
函数的嵌套:在函数中调用其他函数
void func1();
void func2(){
func1();
}
void func1(){
}
注意:在嵌套前需要声明函数
函数的递归:在函数中调用本身
int solve(){
return solve();
}
注意:递归需设立递归边界,并注意栈空间
inline
对于功能相近的函数,其函数名往往相同。而c++允许同名的函数在相同的函数名声明,只要有不同的参数表即可
int add(int x,int y){
return x+y;
}
int add(int x,int y,int z){
return x + y + z;
}
double add(double x,double y){
return x + y;
}
以上三个函数可同时存在,在使用时编译器会通过参数自动选择调用相对应的函数
注意:重载函数的形参个数或者类型必须不同,名称不同,否则无法重载,如下面的反例
int add(int x,int y);
int add(int a,int b);
//无法实现重载
同样,如果参数表相同而函数类型不同,也无法进行重载
int add(int x,int y);
void add(int x,int y);
//无法进行重载
类是指一类事物的抽象化,而对象是指某一类的特定事物
class 类名{
public:
private:
protected:
}
在类中,我们不仅可以定义变量,也可以定义函数
其中,private
和proteceted
中的对象无法被外部直接调用,需要通过外部接口public
访问,例如
class Point{
public:
void setPoint(int x,int y);
void printPoint(int x,int y);
private:
int x,y;
}
在该类中,x
,y
无法直接操作,而我们可以通过public
中的函数对它们进行操作
值得注意的是,类的函数有一个隐藏的形参,指针this
,存储着该对象的地址,同时,该对象中的变量可在函数中直接被调用
在类外定义函数的时候,需要加上作用域,作用域的名称为类名
Point::setPoint(int xx,int yy){
xx = x;
yy = y;
}
也可以用this
指针实现该函数
Point::setPoint(int x,int y){
this -> x = x;
this -> y = y;
}
对于类,有两种特殊的函数,构造函数和析构函数,用于类的初始化和删除
对于构造函数,函数名必须和类名相同,并且不可定义返回值,可以重载,可以使用默认参数,如对上述Point
类,我们可以不使用setPoint
函数,而使用构造函数
class Point{
public: Point(int x,int y);
void printPoint(int x,int y);
private: int x,y;
}
Point::Point(int x,int y):x(x),y(y){}
如此,我们就可以在声明对象的时候直接进行初始化
Point p(1,1);
而如果没有定义构造函数,系统则会调用默认构造函数,其定义如下:
Point(){}
值得注意的是,若定义了构造函数,则无法调用默认构造函数
类中往往有多个构造函数,只是参数表和初始化方式不同,为了避免重复,可以通过一个构造函数定义另一个,形成委托构造函数
Point::Point(int x,int y):this->x(x),this->y(y){}
Point::Point(){Point(0,0);}
如果希望用某个已有的对象来初始化另一个对象,则可以使用复制构造函数,格式如下
Point(const Point&p){
x = p.x;
y = p.y;
}
Point x(1,1);
Point p(x);
上述代码实现了在创建p
时用x
的值复制创建
值得注意的是,形参使用了常引用,可在确保安全性的同时提高效率,这点将在之后详细说明
复制构造函数通过复制的方式构造新的对象,而很多时候被复制的对象仅作复制之用后销毁,在这时,如果使用移动已有对象而非复制对象将大大提高性能。c++11标准引入了左值和右值,定义了右值引用的概念,以表明被引用对象在使用后被销毁,不会再继续使用
直观来看,左值是位于赋值语句左侧的对象变量, 右值是赋值语句右侧的值,不依附于对象,在函数章节中对持久存在的变量的引用,称之为左值引用,相对的对短暂存在可被移动的右值的引用称为右值引用。因此,可通过移动右值引用对象来安全地构建新对象,并且避免冗余复制对象的代价
float n = 6;
float &lr_n = n;
//float &&rr_n = n;
float &&rr_n = n * n;
//float &lr_n = n * n;
注意一个左值对象不能绑定到一个右值引用上,但实际应用中,可能某个对象的作用仅限在初始化其他新对象使用后销毁,标准库utility
中提供了move
函数,将左值对象移动成为右值
float n = 10;
float &&rr_n = std::move(n);
move
函数告诉编译器变量n
转换为当右值来使用,承诺除对n
重新赋值或者销毁它以外,将不再通过rr_n
右值引用以外的方式使用它
基于右值引用的新设定,可以通过移动而不复制实参的高性能方式构建新对象,即移动构造函数。类似于复制构造函数,移动构造函数的参数为该类对象的右值引用,在构造中移动源对象资源,构造后源对象不再指向被移动的资源,源对象可重新赋值或者被销毁
class MyStr{
public:
string s;
MyStr():s(""){};
MyStr(string_s) : s(std::move(_s)) {};
MyStr(MyStr &&str) noexcept
:s(std::move(str.s)){}
};
析构函数会在对象消亡时自动调用,格式如下
~Point(){}
默认析构函数与默认构造函数相同,语句为空
注意,析构函数的参数表必须为空
对于析构函数的应用将在“数据的共享与保护”处说明
类中的成员可以是另一个类的对象,从而实现更抽象的类
例如,我们可以用上述Point
类来定义一个Line
类
class Line{
Line(Point p1,Point p2);
Line(const Line&l);
private:
Point a,b;
}
Line::Line(Point p1,Point p2){
a = p1;
b = p2;
}
Line::Line(const &l){
a = l.a;
b = l.b
}
函数原型中的参数,其作用域始于(
,结束于)
例如:
double area(double r);
其中,r只作用于()
中
函数的形参,在块中(大括号)声明的标识符,其作用域自声明处起,限于块中
void solve(int a){
if(...){
int b;
for(...){
int c;
// c ends
}
// b ends
}
// a ends
}
这种变量也被称作局部变量
namespace
后不标注名字即可,对其他文件不显示不限定作用域的枚举类,即enum color{red,yellow,green};
此时,无法定义枚举名不同,而枚举成员相同的枚举类型
而对于enum class color2{red,yellow,green};
由于限定了作用域,不会造成定义重复
color c = red;
color2 c2 = color2::red;
可见性是从对标识符的引用角度来谈的,表示从内层作用域向外层作用域能“看”到什么
如果标识符在某处可见,就可以在该处引用此标识符
如果某个标识符在外层中声明,且在内层中没有同一标识符声明,则该标识符在内层可见
对于两个嵌套的作用域,如果在内层作用域声明了与外层作用域同名的标识符,则外层作用域的标识符在内层不可见
int i;
int main(){
i = 5;
{
int i;
i = 7;
cout << i << endl;
}
cout << i << endl;
}
//7 5
对象从产生到结束的这段时间就是生存期,在对象生存期内,对象将保持它的值,直到被更新
静态生存期:与程序的运行周期相同,在文件作用域中声明的对象具有这种周期(全局变量),如果在函数内部声明静态生存期对象,要冠以static
动态生存期:块区域中声明,运行至其作用域外失去寿命
int i = 1;//静态全局变量,有全局寿命
void other(){
static int a = 2;
static int b ;
int c = 10;
a += 2; i += 32; c += 5;
cout << "-----Other-----"<< endl;
cout << "i:" << i << "a:" << a << "b:" << b << "c: "<< c << endl;
b = a;
}
int main(){
static int a;//静态局部变量,有全局寿命,局部可见
int b = -10;
int c = 0;
cout << "---Main---\n";
cout << "i:" << i << "a:" << a << "b:" << b << "c: "<< c << endl;
c+=8;
other();
cout <<"---Main---\n";
cout << "i:" << i << "a:" << a << "b:" << b << "c: "<< c << endl;
i+=10;
other();
return 0;
}
/*
---Main---
i:1 a:0 b:-10 c: 0
-----Other-----
i:33 a:4 b:0 c: 15
---Main---
i:33 a:0 b:-10 c: 8
-----Other-----
i:75 a:6 b:4 c: 15
*/
初始化只会对未诞生的对象进行,对于函数other
,在第一次调用后,由于函数中的a
仍有寿命,所以不会重复初始化将其赋值为2
同类对象数据共享,可用静态数据成员
静态数据成员用关键字static
声明,为该类的所有对象共享,静态数据成员具有静态生存期,一般在类外初始化,用::
来指明所属的类
c++11支持静态常量(const
和constexpr
修饰)类内初始化,此时类外仍可定义该静态成员,但不可再次初始化操作
class Point{
public:
Point(int x = 0,int y = 0):x(x),y(y){
count ++;
}
Point(Point &p){
x = p.x;
y = p.y;
count ++;
}
~Point(){count--;}
int getx(){return x;}
int gety(){return y;}
void showCount(){
cout << "count = " << count << endl;
}
private:
int x,y;
static int count;
};
int Point::count = 0;
int main(){
Point a(4,5);
cout << "a :" << a.getx() << "," << a.gety() << endl;
a.showCount();
Point b(a);
cout << "b :" << b.getx() << "," << b.gety() << endl;
b.showCount();
return 0;
}
//a: 4,5
//1
//b: 4,5
//2
在刚才的代码中,我们发现如此只能实现通过对象来访问showCount
函数,十分不合理,所以我们引入静态函数来实现同类对象函数共享
static void showCount(){
cout << count << endl;
}
值得注意的是,静态函数不属于任何对象,不存在this
指针
在实现外部函数的时候,我们常常需要类中的私有成员,若是仅仅通过外部接口来实现会减慢程序的运行效率,所以我们引入友元
友元是一种破坏封装和隐藏的机制,通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本身被隐藏的信息
可使用友元函数和友元类
为了确保封装和隐藏,应少使用友元
class Point{
public:
...
friend float dist(Point &a,Point &b){ //引用可节省空间
double x = a.x - b.x;
double y = a.y - b.y;
return static_cast<float>(sqrt(x*x+y*y));//强制类型转换
private:
int x,y;
}
}
以上为友元函数的实例
class A{
friend class B;
public:
void display(){
cout << x << endl;
}
private:
int x;
}
class B{
public:
void set(int i);
void display();
private:
A a;
}
void B::set(int i){
a.x = i;
}
void B::display(){
a.display();
}
以上为友元类的实例
值得注意的是,友元仅仅是单向的
对于需要共享, 有需要防止改变的数据,应该声明为常类型,不改变对象类型的应定义为常函数
class A{
public:
A(int i,int j){x=i;y=j};
private:
int x,y;
};
A const a(3,4);
若想以常量为参数,可在参数中标明常量
对于常量对象,可调用常函数
void A::print()const{
cout << x << endl << y << endl;
}
注意,常函数无法改变对象中的值
常引用:声明引用的同时加const
修饰,即常引用,该引用在函数中无法被更新
常引用的目的在于可以在节省空间的同时保障数据的安全性。即引用可避免形式参数的新建,对于某些内存占用较大的对象,新建会占用很多不必要的内层,所以用引用可避免,同时定义为常量可防止在函数中对其进行修改
数组是具有一定顺序关系的若干相同类型变量,数组的变量称为该数组的元素
数组的定义:
类型说明符 数组名[常量表达式][常量表达式]...;
如int a[10]
,表示a
为整形数组,有十个元素,a[0],a[1]...,a[9]
数组的使用
数组的遍历
for(const auto &e:a){
cout << e << ends;
}
通过范围for
循环来实现
for(int i=0;i<len;i++){
cout << a[i] << ends;
}
通过访问下标来实现
数组元素在内存中顺次存放,它们的地址是连续的。元素间物理地址上的相邻,对应着逻辑次序上的相邻
列出全部元素的初始值
static int a[10] = {0,1,2,3,4,5,6,7,8,9};
可以只给一部分元素指定初值
int a[10] = {0,1,2,3,4};
在列出全部数组元素初值时,可以不指定数组长度
static int a[] = {0,1,2,3,4,5,6,7,8,9};
按行存放
如int a[3][4]
a[0]
存放着第一行的首地址,即a[0][0]
的地址,同理,a[1],a[2]
也存放着首地址
数组的存储顺序为
a[0][0],a[0][1],a[0][2],a[0][3],a[1][0],a[1][1],a[1][2],a[1][3],a[2][0],a[2][1],a[2][2],a[2][3]
将所有的初值写在一个{}
中,按顺序初始化
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
分行列出二维数组的初值
int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
可以只对部分元素初始化
int a[3][4] = {{1},{0,6},{0,0,11}};
列出全部的初始值,第一位下标个数可以省略
int a[][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int a[][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
内存空间的访问方式分为两种,一种是通过变量名,一种是通过地址,地址即是变量、函数等存放的位置
指针变量的声明
数据类型 *标识符;
int *ptr;
指针运算可通过&
,*
来实现,&
为取地址符,*
为解引用符
取地址符可获取某一变量的地址,如
int x;
cout << &x << endl;
可输出x
的地址
解引用符可获得地址所存储的值
int *p = &x;
*p = 1;
可将x
的值改为1
值得注意的是,在声明语句中,&
表示引用,*
表示指针
指针的赋值
可在声明的时候进行初始化赋值
int *ptr = nullptr;
空指针也可赋为0
,也可为nullptr
对于无初值的指针,应赋为空
数组名实际上是一个不能被赋值的指针,即指针常量
int a[10];
int *ptr = a;
可以声明指向常量的指针
int a;
const int *p1 = &a;
int b;
p1 = &b;
*p1 = 1;//编译错误
上述代码中,最后的语句,*p1 = 1;
不合法,因为p1
的类型为指向常量的指针,无法通过该指针改变变量的值,而该指针可以修改指向的对象
可以声明指针类型的常量,此时指针本身的值不能被改变
int *const p2 = &a;
p2 = &b;//编译错误
指针指向的对象无法被改变
可以定义void
类型的指针,可以存储任何类型的对象地址
指针运算
指针可以进行和整数的加减运算
p1+x
代表p1
向后移动x
个数的地址,减法同理。同时,也可进行自加与自减操作
*(p1+x)
表示p1
后x
位的内容,也可简写成p1[x]
用指针处理数组元素
数组的加减运算的特点使得其十分适合处理一段连续内存空间中的同类数据。而数组恰好符合此特,而数组名就是数组存储的首地址,这样,我们就可以利用指针进行对数组快速高效的访问
把数组作为函数的形参,等价于把指向数组元素类型的指针作为形参,如下列三种写法是等价的
void f(int p[]);
void f(int p[3]);
void f(int *p);
c++标准库引入了begin
函数和end
函数用于获取数组的首地址和尾地址
int a[10];
int i = 0;
for(int *p1 = begin(a);p1<=end(a);p1++){
*p1 = i;
i++;
}
指针数组
如果一个数组的每个元素都是指针变量,那么这个数组就是指针数组,指针数组的每个元素都必须是同一类型的指针
声明一维指针数组的语法形式为
数据类型 *数组名[下标表示式];
例:用指针存储单位矩阵
int line1[] = {1,0,0};
int line2[] = {0,1,0};
int line3[] = {0,0,1};
int *pline[3] = {line1,line2,line3};
用指针作为函数参数
以指针作为参数有以下三个作用
注:如果函数体不需要通过指针改变指针所指向的对象的内容,应在参数表中将其声明为指向常量的指针,这样使得常对象被取地址后也可作为该函数的参数
在程序设计时,当某个函数中以指针或引用作为形参都可以达到同样目的,使用引用可使程序的可读性更好些
指针型函数
当一个函数的返回值是指针类型时,这个函数就是指针型函数,使用指针型函数的最主要目的就是要在函数结束时吧大量的数据从被调函数返回到主调函数中
函数返回数组指针
由于数组不能被复制,因此函数不能返回数组,但可以返回数组
声明方法一:利用别名
using arr = int[10];
arr*func(int i);
方法二:直接声明
int (*foo(int i))[10];
foo(int i)
定义了一个函数foo
,需要一个int i
的参数(*foo(int i))
意味着对函数返回的结果进行解析操作(*foo(int i))[10]
表示解析返回结果得到的是一个大小为10
的数组int (*foo(int i))[10]
说明数组是int
类型方法三:尾置类型
auto foo(int i) -> int(*)[10];
方法四:decltype
声明
int a[] = {0,1,2,3,4};
int b[] = {5,6,7,8,9};
decltype(a)*func(int i){
return (i%2)?&a:&b;
}
int main(){
for(int i=0;i<5;i++){
cout << (*func(2))[i] << ends;
}
return 0;
}
指向函数的指针
同数组名一样,函数名也是表示函数的代码在内存中的起始地址
定义形式:函数数据类型 (*函数名)(参数表)
可实现相似函数的调用,减少代码重复性
int compute(int a,int b,int (*func)(int,int)){return func(a,b);}
int max(int a,int b){return (a>b)?a:b;}
int sum(int a,int b){return a+b;}
res = compute(1,2,&max);//2
res = compute(2,3,&sum);//5
指向常成员函数的指针在声明时应写出const
关键字
虽然通过数组,可以对大量的数据和对象进行有效的管理,但很多情况下,在程序运行之前,并不能够准确的知道数组有多少个元素,于是我们可以使用new
和delete
来进行
``new 类型名T(初始化参数)`,可以不进行初始化
结果值:成功:指针;失败:抛出异常
同时,也可以动态分配数组,new 类型名T[表达式][常量表达式]...()
T
类型对象数组的内存空间,()
中必须为空()
也可以没有返回值同单个变量
double *arr = new double[n];
char (*fp)[3];
fp = new char[n][3];
释放内存操作符delete
delete 指针p
功能:释放指针p所指向的内存。p
必须是new
创建的
为避免内存泄漏,new
出的内存必须进行delete
delete
只会删除内存分配的空间,不会删除指针本身
对于类,new
时调用构造函数,delete
调用析构函数
如果new
时使用[]
创建多维空间,则需delete []
利用new
和delete
,我们可以实现一个动态内存分配的数组,或是可以使用 S T L STL STL中自带的vector
字符串常量的存储
字符串常量是用一对双引号括起来的字符序列,每个字符占1字节,并在末尾添加\0
作为结尾标记。这实际上是一个隐含创建的类型为char
的数组,一个字符串常量就表示这样一个数组的首地址。因此,可以把字符串常量赋给字符串指针,由于常量值是不能改的,应将字符串常量赋给指向常量的指针,例如
const char *string1 = "This is a string.";
对于变量的存取, 可以通过上述方法进行,也可以通过char
数组,并以\0
结束
char str[8] = {'p','r','o','g','r','a','m','\0'};
char str[8] = "program";
char str[] = "program";
c++标准库将面向对象的串的概念加入到c++语言中,预定义了字符串类(string类
),字符串类提供了对字符串进行处理需要的操作。使用string类
需要包含头文件string
严格来说,string
并非一个独立的类,而是类模板的一个特殊化实例
更多信息,可在STL
中学习,此处省略
类的继承和派生的层次结构,可以说是人们对自然界中的事物进行分类、分析和认识的过程在程序中的体现,现实世界中的事物都是相互联系、相互作用的,人们在认识过程中,根据其实际特征,抓住其共同特性和细小差别,利用分类的方法进行分析和描述。最高层是抽象程度最高的,是最具有普遍和一般意义的概念,下层具有了上层的特性,同时加入了自己的新特征,而最下层是最为具体的,上下层之间的关系就可以看作基类与派生类的关系
类的继承,是新的类从已有类那里得到已有的特性,从已有类产生新类的过程就是类的派生,其中,原有的类称为基类或父类,产生的新类称为派生类或子类
派生类的定义
class 派生类名:继承方式 基类名1,继承方式 基类名2,···,继承方式 基类名n{
派生类成员声明;
};
假如,基类Base1 Base2
是已经定义的类,下面的语句定义了一个名为Derived
的派生类,该类由上述两个基类派生而来
class Derived : public Base1,private Base2{
public:
Derived();
~Dervied();
};
吸收基类成员
在c++的类继承中,第一步是将基类的成员全盘接收,这样,派生类实际上就包含了它的全部基类中除构造和析构函数之外的所有成员。
改造基类成员
如果派生类声明了一个和某基类成员同名的新成员,新生的新成员就隐藏或覆盖外层同名成员
添加新的成员
派生类增加新成员使派生类在功能上有所发展
不同继承方式的影响体现在
派生类成员对基类成员的访问权限
通过派生类对象对基类成员的访问权限
三种继承方式
公有继承
私有继承
保护继承
当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问。
也就是说基类的公有成员和保护成员被继承到派生类中访问属性不变,仍作为派生类的公有成员和保护成员,派生类的其他成员可以直接访问它们。在类族之外只能通过派生类的对象访问从基类继承的公有成员,而无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员
class Point{
public:
void initPoint(float x = 0,float y = 0){this -> x = x;this ->y = y;}
void move(float offX,float offY){x+=offX,y+=offY;}
float getX(){return x;}
float getY(){return y;}
private:
float x,y;
};
class Rectangle:public Point{
public:
void initRectangle(float x,float y,float w,float h){
initPoint(x,y);
this->w = w;
this->h = h;
}
float getH()const {return h;}
float getW()const {return w;}
private:
float w,h;
};
主函数部分
int main(){
Rectangle rect;
rect.initRectangle(2,3,20,10);
rect.move(3,2);
cout << rect.getX() << ends << rect.getY();
return 0;
}
//5,5
当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问
也就是说基类的公有和保护成员被继承后作为派生类的私有成员,派生类的其他成员可直接访问它们,但在类外通过派生类的对象无法直接访问它们。无论是派生类的成员还是通过派生类的对象,都无法直接访问从基类继承的私有成员
经过私有继承之后,所有的基类成员都成为派生类的私有成员或不可直接访问的成员,如果进一步派生,基类的全部成员就无法在以后的派生类中直接发挥作用,实际是相当于终止了基类功能的继续派生,出于这种原因,一般情况下私有继承的使用比较少
为了在私有继承的同时,保留一些基类的外部接口特征,则需要重新定义,如下
class Rectangle:private Point{
public:
void move(float offX,float offY){Point::move(offX,offY);}
...
};
注意,声明作用域
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类对象来替代。
类型兼容规则中所指的替代包括以下情况:
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员
class B{...};
class D:public B{...};
B b1,*pb1;
D d1;
派生类对象可以隐含转换成基类对象,即用派生类对象中从基类继承来的成员,逐个赋值给基类对象的成员
b1 = d1;
派生类的对象也可以初始化基类对象的引用
B &rb = d1;
派生类对象的地址也可以隐含转换为指向基类的地址
pb1 = &d1;
由于类型兼容规则的引入,对于基类及其公有派生类的对象,可以使用相同的函数统一进行处理,因为当函数的形参为基类的对象(或引用、指针)时,实参可以是派生类的对象(或指针),而没有必要为每一个类设计单独的模块,大大提高了程序的效率。可以说,类型兼容规则是多态性的重要基础之一。以下是代码示例:
class Base1{
public:
void display()const {cout << "Base1::display()"<<endl;}
};
class Base2:public Base1{
public:
void display() const{cout <<"Base2::display()"<<endl;}
};
class Derived:public Base2{
public:
void display() const{cout <<"Derived::display()"<<endl;}
};
void func(Base1 *ptr){
ptr->display();
}
int main(){
Base1 b1;
Base2 b2;
Derived d;
func(&b1);
func(&b2);
func(&d);
return 0;
}
// B1::display();
// B1::display();
// B1::display();
由于基类的构造函数和析构函数不能被继承,如果在派生类中想要对新增的成员进行初始化,就必须为派生类添加新的构造函数。但是派生类的构造函数只负责对派生类新增的成员进行初始化,对所有从基类继承下来成员,其初始化的工作还是由基类的构造函数完成,析构函数同理
派生类对于基类的很多成员对象是不能直接访问的,因此要完成对基类成员对象的初始化工作,需要通过调用基类的构造函数。派生类的构造函数需要以合适的初值作为参数,其中一些参数要传递给基类的构造函数,用于初始化相应的成员。在构造派生类的对象时,会首先调用基类的构造函数,来初始化它们的数据成员,然后按照构造函数初始化列表中指定的方式初始化派生类新增的成员对象,最后才执行派生类构造函数的函数体
派生类名::派生类名(参数表):基类1(基类1初始化参数表),...,基类n(基类n初始化参数表),成员对象1(成员对象1初始化参数表),...,成员对象m(成员对象m初始化参数表),基本类成员初始化{
派生类构造函数的其他初始化操作;
}
对于使用默认构造函数的基类、成员对象,可以不给出类名
class Derived:public Base1,public Base2,public Base3{
public:
Derived(int a,int b,int c,int d):Base1(a),Base2(b),member1(c),member2(d){}
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
注:在上述基类中,Base1 Base2
的构造函数中有一个参数,而Base3
使用默认构造函数
析构函数没有类型,也没有参数,和构造函数相比情况略为简单些。
派生类的析构函数声明方法和类中的完全相同,只要在函数体中负责把派生类新增的非对象成员的清理工作做好就够了,系统会自动调用基类及对象成员的析构函数来对基类及对象成员进行清理。但它的执行次序和构造函数正好严格相反,首先进行析构函数的函数体,再对派生类新增的类类型成员对象进行清理,最后对所有从基类继承来的成员进行清理
delete
构造函数通过delete
可以实现禁止默认构造函数或者删除复制构造函数以阻止复制的做法,在积累中删除掉的构造函数,在派生类中也对应是删除状态,即如果基类中的默认构造函数、复制构造函数、移动构造函数是删除或者不可访问的,则派生类中对应的成员函数将是被删除的。
在派生类中,成员可以按访问属性划分为4种
在对派生类的访问中,实际上有两个问题需要解决:第一是唯一标识问题,第二个问题是成员本身的属性问题,严格讲应该是可见性问题。我们只能访问一个能够唯一标识的可见成员。如果通过某一个表达式能引用的成员不止一个,称为有二义性
作用域分辨符就是我们经常见到的::
,它可以用来限定要访问的成员所在的类的名称,一般的使用形式是:
类名::成员们
类名::成员名(参数表)
如果存在多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层依然可见,如果内层存在同名标识符,那么外层标识符在内层不可见,这时称内层标识符隐藏了外层同名标识符,这种现象称为隐藏规则
如果派生类中声明了基类中的同名变量,那么基类中的该变量将被隐藏。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用作用域分辨符和基类名来限定
对于多继承情况,首先考虑各个基类之间没有任何继承关系,同时也没有共同基类的情况,如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员。使用对象名可直接访问派生类的成员,使用作用域分辨符和基类名也可访问基类的成员。如果派生类没有新增同名成员,那么就不可以通过对象直接访问该成员
如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来限定
如,有一个基类Base
,声明了数据成员var
和函数func
,由Base
公有派生产生了类Base1
和Base2
,再以Base1
、Base2
作为基类共同公有派生了新类Derived
类,就含有通过Base1
、Base2
继承来的Base
中的同名成员
现在来讨论同名成员的标识与访问问题。间接基类Base
的成员经过两次派生之后,通过不同的派生路径以相同的名字出现在Derived
中,这时如果使用基类名Base
来限定,同样无法表明成员到底是从Base
还是Base2
继承过来,因此必须使用直接基类Base1
或者Base2
的名称来限定,才能够唯一标识和访问程序
当某类的全部或部分直接基类是从另一个共同基类派生而来时,在这些直接基类中从上一级共同基类继承来的成员就拥有相同的名称。在派生类对象中,这些同名数据成员在内存中同时拥有多个副本,同一个函数名会有多个映射。我们可以使用作用域标识符来唯一标识并分别访问它们,也可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个,同一个函数名也只有一个映射
定义形式
class 派生类名:virtual 继承方式 基类名
上述语句声明基类为派生类的虚基类。再多继承情况下,虚基类关键字的作用范围和继承方式相同,只对紧跟其后的基类起作用。声明了虚基类机会,虚基类的成员在进一步派生过程中和派生类一起维护一个内存数据
class Base0{
public:
int var0;
void fun0(){
cout<<"Member of Base0"<<endl;
}
};
class Base1:virtual public Base0{
public:
int var1;
};
class Base2:virtual public Base0{
public:
int var2;
}
class Derived:public Base1,public Base2{
public:
int var;
void func(){
cout << "Memver of Derived" << endl;
}
}
int main(){
Derived d;
d.var0 = 2;
d.fun0();
return 0;
}
// Member of Base0
如果虚基类声明有非默认形式的构造函数,并且没有声明默认形式的构造函数,就会比较麻烦。这时,在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始表中列出对虚基类的初始化表中列出对虚基类的初始化,例如,上述代码就应改成如下形式
class Base0{
public:
Base0(int var):var0(var){}
int var0;
void fun0(){
cout<<"Member of Base0"<<endl;
}
};
class Base1:virtual public Base0{
public:
Base1(int var):Base0(var){}
int var1;
};
class Base2:virtual public Base0{
public:
Base2(int var):Base0(var){}
int var2;
}
class Derived:public Base1,public Base2{
public:
Derived(int var):Base0(var),Base1(var),Base2(var);
int var;
void func(){
cout << "Memver of Derived" << endl;
}
}
int main(){
Derived d;
d.var0 = 2;
d.fun0();
return 0;
}
我们可以将Derived
称为最远派生类,在建立对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数进行初始化的,而且,只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用都会被自动忽略
构造一个类的对象的一半顺序是
多台是指相同的消息被不同类型的对象接收时导致不同的行为,所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。最简单的例子就是运算符,如+
可以应用在整数类型、浮点数类型等,同样的消息——相加,被不同类型的对象接收后,不同类型的变量采用不同的方法进行加法运算,如果是不同类型的变量想加,如浮点数和整形数,则要先将整形数转换为浮点数,然后再进行加法运算,这就是典型的多态现象
多态性可分为四类:重载多态、强制多态、包含多态、参数多态。前两种统称为专用多态,后两种称为通用多态
多态从实现的角度来讲可以分为两类:编译时的多态和运行时的多态。前者是在编译过程中确定了同名操作的具体操作对象,而后者则是在程序运行过程中才动态地确定操作所针对的具体对象。这种确定操作的具体对象的过程就是绑定。绑定是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程,用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。按照绑定进行的阶段的不同,可以分为两种不同的绑定方法:静态绑定和动态绑定,这两种绑定过程中分别对应着多态的两种实现方式
除了少数几个外,其余运算符都可以重载,而且只能重载c++中已经存在的运算符
重载之后运算符的优先级和结合性都不会改变
运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造,一般来讲,重载的功能应当与原有功能类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型
有些运算符不能重载,它们是类属关系运算符.
、成员指针运算符.*
、作用域运算符::
、三目运算符?:
运算符的重载形式有两种,重载为类的非静态成员函数和重载为非成员函数。
返回类型 类名:: operator 运算符(形参表){
}
//重载为类的成员函数
返回类型 operator 运算符(形参表){
}
//重载为非函数成员
operator
是定义运算符重载函数的关键字++
--
除外),当重载为非成员函数时,参数个数与原操作数相同然而在实际应用中,总是通过类的对象来访问重载的运算符,所以重载为类的成员函数更加实用。如果是双目运算符,左操作数是对象本身的数据,由this
指针指出,右操作数则需要通过运算符重载函数的参数表来传递,如果是单目运算符,就不需要任何参数
x B y
,其中x
是A
类的对象,则应当把B
重载为A
类的成员函数,该函数只有一个形参,形参的类型是y
所属的类型U
,用来实现表达式U x
,则应当重载为成员函数,没有形参++
,--
,如果将它们重载为类的成员函数,用来实现x++ x--
,那么运算符就应当重载为成员函数,这时函数要带一个整型形参,这里的形参在运算中不起任何作用,只是用于区分前置和后置class Clock{
public:
Clock(int hour = 0,int minute = 0,int second = 0);
void showTime() const;
Clock& operator++();
Clock operator++(int);
private:
int hour,minute,second;
};
Clock::Clock(int hour = 0,int minute = 0,int second = 0):hour(hour),minute(minute),second(second){}
void Clock::showTime(){
cout << "Hour: "<< hour << ends << "Minute: "<< ends << "Second: "<< second << endl;
}
Clock& Clock::operator++(){
second ++;
if(second >= 60){
second -= 60;
minute ++;
}
if(minute >= 60){
minute -= 60;
hour ++;
}
hour %= 24;
return *this;
}
Clock Clock::operator++(int){
Clock c = *this;
++(*this);
return c;
}
重载为非成员函数
B
,如果要实现x B y
,其中x y
中只要有一个具有自定义类型,就可以将B
重载为非成员函数U
,可以重载为非成员函数,函数的形参为类的对象++
和--
,可以重载为非成员函数,并在参数表中加int
<<
操作符的左操作数是ostream
类型的引用,ostream
是cout
类型的一个基类,右操作数是类的引用。函数把通过第一个参数传入的ostream
对象以引用形式返回,这是为了支持形如cout << c1 << c2
的连续输出运算符的两种重载形式各有千秋。成员函数的重载方式更加方便,但又是出于以下原因,需要使用非成员函数的重载方式
=[]()->
只能被重载为成员函数,而且派生类中的=
总会隐藏基类中的=
运算符函数现在我们遇到一个问题:如何利用一个循环结构处理同一类族中不同类的对象。为了解决这种问题,我们引入虚函数
虚函数是动态绑定的基础,虚函数必须是非静态的成员函数,虚函数经过派生之后,在类族中就可以实现运行过程中的多态
根据赋值兼容原则,可以使用派生类的对象代替基类对象。如果用基类类型的指针指向派生类对象,就可以通过这个指针访问该对象,问题是访问到的只是从基类继承来的同名成员。解决这一问题的方法是,如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数
如下代码
void fun(Base1 *ptr){
ptr -> display();
}
int main(){
Base1 b1;
Base2 b2;
Derived d;
fun(&ba1);
fun(&ba2);
fun(&d);
}
其中,Base1
为Base2
的基类,Base2
为Derived
的基类,由于fun
的形参是指向Base1
类型的指针,所以只会调用Base1
类型的display()
声明语法为:
virtual 函数类型 函数名(形参表);
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候
运行过程中的多态要满足三个条件
我们考虑改写上述代码
#include
using namespace std;
class Base1{
public:
virtual void display() const;
};
void Base1::display()const{
cout << "Base1" << endl;
}
class Base2:public Base1{
public:
virtual void display() const;
};
void Base2::display()const{
cout << "Base2" << endl;
}
class Derived:public Base2{
public:
void display() const;
};
void Derived::display()const{
cout << "Derived" << endl;
}
void func(Base1 *ptr){
ptr -> display();
}
int main(){
Base1 b1;
Base2 b2;
Derived d;
func(&b1);
func(&b2);
func(&d);
return 0;
}
程序中类Base1 Base2 Derived
属于同一个类族,而且通过公有派生而来,因此满足赋值兼容规则,同时,基类将函数声明为虚函数,程序中使用对象指针来访问函数成员,这样的绑定过程就是在运行中完成,实现了运行中的多态。通过基类类型的指针就可以访问到正在指向的对象的成员,这样,能够对同一类族的对象进行统一的处理,抽象程度更高,程序更简洁、更高效
在本程序中,派生类并没有显式给出虚函数声明,这时系统就会遵循以下规则来判断派生类的一个函数成员是不是虚函数
如果从名称、参数、返回值三个方面检查之后,派生类的函数满足了上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的所有重载形式
具有纯虚函数的类叫做抽象类,抽象类不能定义其对象
纯虚函数的声明:
virtual 函数类型 函数(形参表) = 0;
纯虚函数在该基类中没有定义具体的操作内容,要求各派生类根据自己需要定义自己的板本
抽象类的作用
override
与final
override
显式函数覆盖
声明该函数必须覆盖基类的虚函数,编译器可发现“未覆盖”错误
覆盖要求:函数签名完全一致
class Base{
virtual void func(int)const;
};
class Derived : public Base{
virtual void func(int) override;//报错,无法覆盖
virtual void func(int)const override;//正确
}
final
用来避免类被继承,或是基类的函数被覆盖
class Base1 final{};
class Derived1 : public Base1{};//编译错误,无法继承Base1
class Base2{
virtual void func() final;
};
class Derived : public Base2{
void func();//错误,func不允许被覆盖
};
为了实现代码重用,代码必须具有通用性。通用代码需要不受数据类型的影响,并且可以自动适应数据类型的变化。这种程序设计类型称为参数化程序设计。模板是c++支持参数化程序设计的工具,通过它可以实现参数化多态性。所谓参数化多态性,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象
很多情况下,一个算法是可以处理多种数据类型的,但是用函数实现算法时,即使设计为重载函数也只是使用相同的函数名,函数体仍然要分别定义。使用函数模板则可避免这种情况,其定义形式是
template <模板参数表>
类型名 函数名(参数表){
函数体
}
所有函数模板的定义都是用关键字template
开始的,该关键字之后是使用<>
扩起来的模板参数表,模板参数表由用逗号分隔的模板参数构成,可以包括以下内容
class
或者typename
标识符,指明可以接受一个类型参数,这些类型参数代表的是类型,可以是预定义或自定义类型类型操作符 标识符
,指明可以接受一个由类型操作符所规定类型的常量作为参数template<参数表> class
,表明可以接收一个类模板名作为参数类型参数可以用来指定函数模板本身的形参类型、返回值类型,以及声明函数中的局部变量,函数模板中的函数体的定义方式与定义普通函数类似
template <typename T>
T abs(T x){
return x<0?-x:x;
}
再如:
template<typename T>
void outputArray(const T* array,int count){
for(int i=0;i<count;i++) cout << array[i] << " ";
cout << endl;
}
注意,当模板为类类型时,应当对其用到的运算符进行重载
使用类模板使得用户可以为类定义一种模式,使得类中的某些数据成员、某些成员函数的参数、函数返回值、局部变量能取到不同的类型
类模板声明的语法形式是:
template<模板参数表>
class 类名{
};
template <typename T = double> //默认参数
class Point{
public:
Point(T _x = 0,T _y = 0):x(_x),y(_y){}
private:
T x;
T y;
}
如果需要在类模板外定义其成员函数,则要用以下形式
template<模板参数表>
类型名 类名<模板参数标识符列表>:: 函数名(参数表);
使用模板类来建立对象时,应按以下形式声明
模板名<模版参数表> 对象名1,...,对象名n;
如下
#include
using namespace std;
template <class T>
class Store{
private:
T item;
bool haveValue;
public:
friend T;
Store();
T &getElem();
void putElem(const T &x);
};
template <class T>
Store<T>::Store():haveValue(false){ }
template <class T>
T &Store<T> :: getElem(){
if(!haveValue){
exit(1);
}
return item;
}
template <class T>
void Store<T>::putElem(const T &x){
haveValue = true;
item = x;
}
struct Student{
int id;
float gpa;
};
int main(){
Student g = {1000,23};
Store<Student> s;
s.putElem(g);
cout << s.getElem().id << " " << s.getElem().gpa << endl;
}
#ifndef ARRAY_H
#define ARRAY_H
#include
template <class T>
class Array{
private:
T* list;
int size;
public:
Array(int sz = 50);
Array(const Array<T>&x);
~Array();
Array<T>& operator = (const Array<T> &x);
T& operator[](int i);
const T& operator[](int i)const;
operator T*();
operator const T*() const;
int getSize()const;
void resize(int sz);
};
template <class T> Array<T>::Array(int sz){
size = sz;
list = new T[size];
}
template <class T> Array<T> :: ~Array(){
delete [] list;
}
template <class T>
Array<T>::Array(const Array<T> &x){
size = x.size;
list = new T[size];
for(int i=0;i<size;i++){
list[i] = x.list[i];
}
}
template<class T>
Array<T>& Array<T>::operator = (const Array<T> &x){
if(&x!=this){
if(size!=x.size){
delete[] list;
size = x.size;
list = new T[size];
}
for(int i=0;i<size;i++)
list[i] = x.list[i];
}
return *this;
}
template<class T>
Array<T>::Array(const Array<T> &a){
size = a.size;
list = new T[size];
for(int i=0;i<size;i++)
list[i] = a.list[i];
}
template<class T>
T& Array<T>::operator[](int i){
return list[i];
}
template<class T>
const T& Array<T>::operator[](int i)const{
return list[i];
}
template<class T>
void Array<T>::resize(int sz){
if(sz == size) return;
T* newList = new T[sz];
int n = (sz < size) ? sz : size;
for(int i=0;i<n;i++)
newList[i] = list[i];
delete[]list;
list = newList;
size = sz;
}
template<class T>
Array<T>::operator T*(){
return list;
}
template<class T>
Array<T>::operator const T*()const{
return list;
}
template <class T>
int Array<T>::getSize()const{
return size;
}
#endif
+ -
等运算符而言,即使其返回值不是引用,也可以作为左值,可利用a+b
直接调用其成员函数。不过如果对该左值赋值,则没有意义,所以可以在重载+ -
等运算符时,令其返回常对象T
的对象隐含或显式地转化为S
类型,可以将operator S
定义为T
的成员函数,这样,在把T
类型对象显式隐含转换成S
类型,或用static_cast
显式转换时,该成员函数会被调用。转换操作符的重载函数不用指定返回值的类型,这是由于这种情况下重载函数的返回类型与操作符名称一致节点类
#ifndef NODE_H
#define NODE_H
template<class T>
class Node{
private:
Node<T>* next;
public:
T data;
Node(const T& data,Node<T>*next = nullptr);
void insertAfter(Node<T> *p);
Node<T>* deleteAfter();
Node<T>* nextNode();
const Node<T>*nextNode()const;
};
template<class T>
Node<T>*Node<T>::nextNode(){
return next;
}
template<class T>
const Node<T>* Node<T>::nextNode()const{
return next;
}
template<class T>
Node<T>::Node(const T& data,Node<T>*next = nullptr){
this -> data = data;
this -> next = next;
}
template<class T>
void Node<T>::insertAfter(Node<T>*p){
p->next = next;
next = p;
}
template<class T>
Node<T> *Node<T>::deleteAfter(){
Node<T> *temptr = next;
if(next == 0) return 0;
next = temptr -> next;
return temptr;
}
#endif
链表类
#ifndef LINKEDLIST_H
#define LINKEDLIST_H
#include"Node.h"
template<typename T>
class Linkedlist{
private:
Node<T>*front,*rear;//表头和表尾指针
Node<T>*prevPtr,*currPtr;//记录表当前遍历位置的指针
int size;
int position;//当前元素在表中的序号位置
//函数成员
//生成新节点,数据域为item,指针域为ptrNext
Node<T>*newNode(const T&item,Node<T>*ptrNext=nullptr);
//释放节点
void freeNode(Node<T> *p);
//将链表L复制到当前表
void copy(const Linkedlist<T>&L);
public:
LinkedList();//构造函数
LinkedList(const LinkedList<T>&L);//复制构造函数
~LinkedList();//析构函数
LinkedList<T> &operator={const LinkedList<T>&L};//赋值运算符重载
int getSize()const;//返回链表大小
bool isEmpty()const;//返回链表为空
void reset(int pos=0);//初始化游标的位置
void next();//移动右标到下一位
bool endOfList()const;//返回在表尾
int currentPosition()const;//返回现在游标的位置
void insertFront(const T& item);//在表头插入结点
void insertRear(const T& item);//在表尾插入结点
void insertAt(const T& item);//在当前结点之前插入
void insertAfter(const T& item);//在当前结点之后插入
T deleteFront();//删除头结点
void deleteCurrent();//删除当前结点
T&data();
const T&data()const;
void clear();//清空列表,释放所有节点的内存
};
#endif
具体实现略
#ifndef STACK_H
#define STACK_H
template<class T,int SIZE = 50>
class Stack{
private:
T list[SIZE];
int top;
public:
Stack();
void push(const T&item);
T pop();
void clear();
const T&peek()const;//访问栈顶元素
bool isEmpty()const;
bool isFull()const;
};
template<class T,int SIZE>
Stack<T,SIZE> :: Stack() :top(-1){}
template<class T,int SIZE>
void Stack<T,SIZE>::push(const T&item){
lisk[++top] = item;
}
template<class T,int SIZE>
T Stack<T,SIZE>::pop(){
return list[top--];
}
template<class T,int SIZE>
const T& Stack<T,SIZE>::peek()const{
return list[top];
}
template<class T,int SIZE>
bool Stack<T,SIZE>::isEmpty()const{
return top==-1;
}
template<class T,int SIZE>
bool Stack<T,SIZE>::isFull()const{
return top == SIZE-1;
}
template<class T,int SIZE>
void Stack<T,SIZE>::clear(){
top = -1;
}
#endif
本程序实现一个简单的整数计算器,能够进行加减乘除和乘方运算,使用时算式采用后缀输入法,每次计算在前次结果基础上进行,若要将前次运算结果清除,可输入c
,输入q
时程序结束
//Calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
#include"Stack.h"
class Calculator{
private:
Stack<double>s;
void enter(double num);
bool getTwoOperands(double &op1,double &op2);
void compute(char op);
public:
void run();
void clear();
};
#endif
//Calculator.cpp
#include"Calculator.h"
#include
#include
#include
using namespace std;
void Calculator:: enter(double num){
s.push(num);
}
bool Calculator::getTwoOperands(double &op1,double &op2){
if(s.isEmpty()){
cerr << "Missing operand!" << endl;
return false;
}
op1 = s.pop();
if(s.isEmpty()){
cerr << "Missing operand!" << endl;
return false;
}
op2 = s.pop();
return true;
}
void Calculator::compute(char op){
double op1,op2;
bool result = getTwoOperands(op1,op2);
if(!result){s.clear();return;}
switch(op){
case '+':
s.push(op2 + op1);
break;
case '-':
s.push(op2 - op1);
break;
case '*':
s.push(op2 * op1);
break;
case '/':
s.push(op2 / op1);
case '^':
s.push(pow(op2,op1));
break;
default:
cerr << "Unrecognized operator" << endl;
break;
}
cout << "=" << s.peek() << " ";
}
inline double stringToDouble(const string &str){
istringstream stream(str);
double result;
stream >> result;
return result;
}
void Calculator::run(){
string str;
while(cin >> str , str != "q" ){
switch(str[0]){
case 'c':
s.clear();
break;
case '-':
if(str.size()>1)
enter(stringToDouble(str));
else
compute(str[0]);
break;
case '+':
case '*':
case '/':
case '^':
compute(str[0]);
break;
default:
enter(stringToDouble(str));
break;
}
}
}
void Calculator::clear(){
s.clear();
}
//Calculator_test.cpp
#include"Calculator.h"
int main(){
Calculator c;
c.run();
return 0;
}
#ifndef QUEUE_H
#define QUEUE_H
template<class T,int SIZE = 50>
class Queue{
private:
int front,rear,count;
T list[SIZE];
public:
Queue();
void insert(const T&item);
T remove();//出队
void clear();
const T&getFront()const;
int getLength()const;
bool isEmpty()const;
bool isFull()const;
};
template<class T,int SIZE>
Queue<T,SIZE>::Queue():front(0),rear(0),count(0){}
template<class T,int SIZE>
void Queue<T,SIZE>::insert(const T&item){
count++;
list[rear] = item;
rear = (rear+1) % SIZE;//循环队列
}
template<class T,int SIZE>
T Queue<T,SIZE>::remove(){
int temp = front;
count --;
front = (front + 1) % SIZE;
return list[temp];
}
template<class T,int SIZE>
void Queue<T,SIZE>::clear(){
count = rear = front = 0;
}
template<class T,int SIZE>
const T& Queue<T,SIZE>::getFront()const{
return list[front];
}
template<class T,int SIZE>
int Queue<T,SIZE>::getLength()const{
return count;
}
template<class T,int SIZE>
bool Queue<T,SIZE>::isEmpty()const{
return count==0;
}
template<class T,int SIZE>
bool Queue<T,SIZE>::isFull()const{
return count == SIZE;
}
#endif
泛型程序设计就是编写不依赖于具体数据类型的程序。c++中,模版是泛型程序设计的主要工具
泛型程序设计的主要思想是将算法从特定的数据结构中抽象出来,使算法成为通用的、可以作用于不同的数据结构
而在一些算法中,数据结构需要有某些特性,我们用概念来描述泛型程序设计中作为参数的数据类型所需具备的功能,它的外延是具备这些功能的所有数据结构。具备一个概念所需要功能的数据类型称为这一概念的一个模型,对于两个不同的概念A和B,如果概念A所需求的所有功能也是B需求的功能,那就说B是A的子概念
STL定义了一套概念体系,为泛型程序设计提供了逻辑基础
基本组件:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OZ2Ecwio-1666665427884)(/Users/zhiweijin/Library/Application Support/typora-user-images/image-20220924224130350.png)]
容器是指容纳、包含一组元素的对象
基本容器类模板
顺序容器
array、vector、deque、forward_list、list
有序关联容器
set、multiset、map、multimap
无序关联容器
unordered_set、unordered_multiset、unordered_map、unordered_multimap
容器适配器(受限制的容器)
stack、queue、priority_queue
迭代器是泛化的指针
提供了顺序访问容器中每个元素的方式
可以使用++
运算符来获得指向下一个元素的迭代器
可以使用*
运算符来访问一个迭代器所指向的元素,如果元素类型是类或结构体,还可以使用->
运算符直接访问该元素的一个成员
有些迭代器还支持通过’–'运算符获得指向上一个元素的迭代器
迭代器是泛化的指针,指针本身也是一种迭代器
例:transform算法
first
和last
两个迭代器所指向的元素op
的参数result
迭代器指向的是输出的最后一个元素的下一个位置,transform会将该迭代器返回template <class InputIterator,class OutputIterator,class UnaryFunction>
OutputIterator transform(InputIterator first,InputIterator last,OutputIterator result,UnaryFunction op){
for(;first != last;++first,++result){
*result = op(*first);
}
return result;
}
()
运算符的类的对象都可以作为函数对象使用
#include
#include
#include
#include
#include
using namespace std;
int main(){
const int N = 5;
vector<int> s(N);
for(int i=0;i<N;i++)
cin >> s[i];
transform(s.begin(),s.end(),ostream_iterator<int>(cout," "),negate<int>());
return 0;
}
输入流迭代器
输入流迭代器用来从一个输入流中连续地输入某种类型的数据,它是一个类模版,如:
template
由于STL设计得非常灵活,很多STL模版都有三四个模版参数,但排在后面的参数一般都有默认的参数值,绝大部分程序中都会省略这些参数而使用它们的默认值。例如,istream_iterator
实际上有多达4个模板参数,我们只给出一个默认值的模板参数,后面的模板直接省略
其中,T
是使用该迭代器从输入流中输入数据的类型,类型T
要满足两个条件:有默认构造函数、对该类型的数据可以使用>>
从输入流输入,一个输入流迭代器的实例需要由下面的构造函数来实现
istream_iterator(stream& in)
在该构造函数中,需要提供用来输入数据的输入流作为参数。一个输入流迭代器实例可以支持*
,->
,++
等几种运算符,用*
可以访问刚刚读取的数据,用++
可以从输入流中读取下一个元素,若T
是类类型,用->
可以直接访问刚刚读取元素的成员。判断一个输入流是否结束,istream_iterator
类模版有一个默认构造函数,用该函数构造出的迭代器指向的就是输入流的结束为止,将一个输入流与这个迭代器进行比较就可以判断输入流是否结束
输出流迭代器
输入流迭代器也是一个类模版,例如template
其中的T
表示向输出流输出数据的类型
可用下面两种构造函数来构造
ostream_iterator(ostream& out);
ostream_iterator(ostream& out,const char* delimiter);
其中,out
是输出流,而delimiter
表示分隔符
输出流迭代器的解引用只能作为左值
二者都属于适配器
#include
#include
#include
using namespace std;
double square(double x){
return x*x;
}
int main(){
transform(istream_iterator<double>(cin),istream_iterator<double>(),ostream_iterator<double>(cout,"\t"),square);
cout << endl;
return 0;
}
STL根据迭代器的功能,将迭代器分为了5类,这5类对应5个概念
输入迭代器可以用来从序列中读取数据,但是不一定能够向其中写入数据。输入迭代器支持对序列进行不可重复的单向遍历。前面介绍的输入流迭代器就是一种输入迭代器
需要注意的是,如果p1==p2
,并不能保证++p1==++p2
,由于这一点,用输入迭代器读入的序列不保证是可重复的
输出迭代器允许向序列中写入数据,但是并不保证可以从其中读取数据。输出迭代器也支持对序列进行单向遍历,前面介绍的输出流迭代器就是一种输出迭代器
另外,使用输出迭代器,写入元素的操作和使用自增操作必须是交替进行,否则其行为都是不确定的
前向迭代器这一概念是输入迭代器和输出迭代器这两个概念的字概念,它既支持数据读取,也支持数据写入,前向迭代器支持对序列进行可重复的单向遍历
它去掉了输入迭代器和输出迭代器这两个概念中的一些不确定性,对于向前迭代器,++p1 == ++p2
是一定成立的,也没有自增和写入必须交替进行的情况
双向迭代器这一概念是单向迭代器的子概念,在单向迭代器的基础上,又支持反向移动,即自减
随机访问迭代器这一概念是双向迭代器的子概念,在双向迭代器的基础上,它又支持直接将迭代器向前或向后移动n
个元素,因此随机访问迭代器的能力几乎和指针一样
STL算法中的形参往往包括一对输入迭代器,用它们所构成的区间来表示输入数据的序列。
设p1 p2
是两个输入迭代器,以后将使用[p1,p2)
形式来表示它们所构成的区间,这样一个区间是一个有序序列,包括p1
和p2
两个迭代器所指向元素之间所有元素但不包括p2
所指向的元素。当且仅当对p1
进行了n次++
运算后,表达式p1==p2
的值为true
,此时[p1,p2)
才是一个合法区间
综合运用迭代器的示例
#include
#include
#include
#include
using namespace std;
template <class T,class InputIterator,class OutputIterator>
void mySort(InputIterator first,InputIterator last,OutputIterator result){
vector<T> s;
for(;first != last;first++)
s.push_back(*first);
sort(s.begin(),s.end());
copy(s.begin(),s.end(),result);
}
int main(){
double a[5] = {1.2,2.4,0.8,3.3,3.2};
mySort<double>(a,a+5,ostream_iterator<double>(cout," "));
cout << endl;
mySort<int>(istream_iterator<int>(cin),istream_iterator<int>(),ostream_iterator<int>(cout," "));
}
STL为迭代器提供了两个辅助函数模版,advance
和distance
,它们为所有迭代器提供了一些原本只有随机访问迭代器才有的访问能力:前进或后退多个元素,以及计算两个迭代器之间的距离
advance
函数的模版原型是
template<class InputIt,class Distance>
void advance(InputIt& iter,Distance n);
distance
函数的模版原型是
template<class FirstIt,class LastIt>
void distance(FirstIt& first,LastIt last);
STL有13种容器,每种都具有不尽相同的功能和用法
设S
表示一种容器类型,s1 s2
都是S
类型的实例,容器支持的基本功能如下:
S s1
s1 op s2
,其中,op
为比较运算符,可对两个容器之间的元素按字典序进行比较s1.begin()
s1.end()
s1.clear()
s1.empty()
s1.size()
s1.swap(s2)
在前面的示例程序中,由于我们都是直接把一个容器的begin()
和end()
函数的返回值提供给了一个算法,算法的参数类型由编译器自动解析,因此无须显式写出迭代器类型。但有时显式写出一个容器的迭代器类型还是有必要的,与类型S
的容器相关的迭代器类型可以用下面的方法表示
S::iterator
表示与S
相关的普通迭代器类型,迭代器指向元素的类型为T
S::const_iterator
表示与S
相关的常迭代器类型,迭代器指向元素的类型为const T
,因此无法通过迭代器改写元素
当使用迭代器不需要写访问时,建议使用s1.cbegin() s1.cend()
容器作为一种STL的概念,有许多子概念。容器分为顺序容器和关联容器,这就是容器的两个子概念,这种划分是基于容器中元素的组织方式的,另一方面,按照容器与所关联的迭代器类型划分,容器又具有可逆容器这一子概念,可逆容器又具有随机访问容器这一子概念
使用一般容器的begin() end()
成员函数所得到的迭代器都是前向迭代器,也就是说可以对容器的元素进行单向的遍历,而可逆容器所提供的迭代器是双向迭代器,可以对容器的元素进行双向的遍历
事实上,STL提供的标准容器都至少是可逆容器,但有些非标准的模版库提供诸如slist
单向链表这样的仅提供前向迭代器的容器
对一个可逆容器进行逆向遍历时,可以通过对其迭代器使用--
运算来进行,但有时这样做不够方便,因为STL
算法的输入都是用正向区间来表示的,为此,STL为每个可逆容器都提供了逆向迭代器,逆向迭代器可以通过下面的成员函数得到
s1.rbegin()//得到指向容器的最后一个元素的逆向迭代器
s1.end()//得到指向容器第一个元素的前一个位置的逆向迭代器
逆向迭代器的类型名的表示方式如下
S::reverse_iterator//表示与S相关的普通迭代器类型,迭代器指向元素的类型为`T`
S::reverse_iterator
逆向迭代器实际上是普通迭代器的适配器,逆向迭代器的++
运算被映射为普通迭代器的--
,反之同理
一个迭代器和它的逆向迭代器之间可以互相转换。逆向迭代器都有一个构造函数,用它可以构造一个迭代器的逆向迭代器,如可以用S::reverse_iterator r1(p1)
就可以得到逆向迭代器;另一方吧,逆向迭代器提供一个成员函数base
,用它可以得到用于构造该逆向函数的迭代器,r1.base()
即p1
,关于迭代器与逆向迭代器,有以下的等式关系
s1.rbegin() == S::reverse_iterator(s1.end()) , s1.rbegin().base() == s1.end();
s1.rend() == S::reverse_iterator(s.begin()) , s1.rend().base() == s1.begin();
随机访问容器所提供的迭代器是随机访问迭代器,支持对容器的元素进行随机访问。使用随机访问容器,可以直接通过一个整数来访问容器中的指定元素
s1[n]
等价于 s1.begin()[n]
下表给出了容器所属概念
容器名 | 中文名 | 所属概念 |
---|---|---|
vector | 向量 | 随机访问容器、顺序容器 |
deque | 双端队列 | 随机访问容器、顺序容器 |
list | 列表 | 可逆容器、顺序容器 |
forward_list | 单向链表 | 单向访问容器、顺序容器 |
array | 数组 | 随机访问容器、顺序容器 |
set | 集合 | 可逆容器、关联容器 |
multiset | 多重集合 | 可逆容器、关联容器 |
map | 映射 | 可逆容器、关联容器 |
multimap | 多重映射 | 可逆容器、关联容器 |
unordered_set | 无序集合 | 可逆容器、关联容器 |
unordered_multiset | 无序多重集合 | 可逆容器、关联容器 |
unordered_map | 无序映射 | 可逆容器、关联容器 |
unordered_multimap | 无序多重映射 | 可逆容器、关联容器 |
array
对象的大小固定,forward_list
有特殊的添加和删除操作构造函数
S s(n,t)
构造一个由n
个t
元素构成的容器实例s
S s(n)
构造一个有n个元素的容器实例s
,每个元素都是T()
S s(q1,q2)
使用[q1,q2)
区间内的数据作为s
的元素构造s
,其中q1 q2
为迭代器
赋值函数
assign
将指定的元素赋给顺序容器,顺序容器中原先的元素会被清除,赋值函数的形式和构造函数相同
插入函数
可以一次插入一个或多个指定元素,也可以将一个迭代器区间中的序列插入,通过一个指向当前容器元素的迭代器来指示插入位置,返回值为指向新插入位置的元素中第一个元素的迭代器
s.insert(p1,t)
在s
容器中p1
所指向的位置插入一个t
的复制,插入后的元素夹在原p1
,和p1-1
所指向的元素之间
s.insert(p1,n,t)
在s
中p1
指向的位置插入n
个t
的复制
s.insert(p1,q1,q2)
将[q1,q2)
的元素插入
s.emplace(p1,args)
,将参数args
传递给T
的构造函数构造新元素t
,在s
容器中插入t
其他函数
erase,clear,pop_front,pop_back,front,back,resize
特点
向量的容量
s.capacity()
:返回当前容量s.reserve(n)
:将容量扩充至n
s.shrink_to_fit()
:回收未使用的元素空间,即size
和capacity
函数返回值相等特点
例:先按从大到小顺序输出奇数,再按照从小到大顺序输出偶数
#include
using namespace std;
int main(){
istream_iterator<int> i1(cin),i2;
vector<int> s1(i1,i2);
sort(s1.begin(),s1.end());
deque<int> s2;
for(vector<int>::iterator iter = s1.begin(); iter != s1.end(); iter++){
if(*iter %2 == 0)
s2.push_back(*iter);
else
s2.push_front(*iter);
}
copy(s2.begin(),s2.end(),ostream_iterator<int>(cout," "));
cout << endl;
return 0;
}
特点
在任意位置插入和删除元素都很快
不支持随机访问
接合splice
操作
s1.splice(p,s2,q1,q1)
:将s2中[q1,q2)
移动到p
之前
array
是对内置数组的封装,提供了更安全、更方便的使用数组的方式array
的对象的大小是固定的,定义时除了需要指定元素类型,还需要指定容器大小insert_after
、emplace_after
和erase_after
操作以顺序容器为基础构造一些常用数据结构,是对顺序容器的封装
在顺序容器(除array
)中插入元素,还可以通过插入迭代器。插入迭代器是一种适配器,使用它可以通过输出迭代器的接口来向指定元素的指定位置插入元素,因而如果在调用STL算法时使用输出迭代器,可以将结果顺序插入到容器的指定位置,而无需覆盖已有的元素。插入迭代器有3种:
template<class Container> class front_insert_iterator
template<class Container> class back_insert_iterator
template<class Container> class insert_iterator
插入迭代器的实例可以通过构造函数来实现,但一般无须直接调用构造函数,而是可以通过下面这个辅助函数
template<class Container>
front_insert_iterator<Container>front_inserter(Contain&s);
对于另外两种迭代器同理
由于辅助函数是函数模版,调用时可以直接根据实参自动推出类型参数,可以使代码更加简洁
关于关联容器,详细见数据结构
与c语言一样,c++语言中没有输入输出语句,但c++标准库中有一个面向对象的输入输出软件包,它就是I/O
流类库,流是I/O
流类的中心概念
I/O
流的概念及流类库结构当程序与外界环境进行信息交换时,存在着两个对象,一个是程序中的对象,另一个是文件对象程序建立一个流对象,并指定这个流对象与某个文件对象建立联系,程序操作流对象,流对象通过文件系统对所连接的文件对象产生作用。由于流对象是程序中的对象与文件对象进行交换的洁面,对程序对象而言,文件对象有的特性,流对象也有,所以程序将流对象看作是文件对象的化身
凡是数据从一个地方传输到另一个地方的操作都是流的操作。
一般意义下的读操作在流数据抽象中被称为从流中提取,写操作被称为向流中插入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WbA9clS3-1666665427885)(/Users/zhiweijin/Library/Application Support/typora-user-images/image-20220927181543028.png)]
一个输出流对象是信息流动的目标,最重要的3个输出流是ostream,ofstream,ostringstream
预先定义的ostream
类对象用来完成向标准设备的输出
cout
是标准输出流cerr
是标准错误输出流,没有缓冲,发送给它的内容立即被输出clog
类似于cerr
,但是有缓冲,缓冲区满时被输出ofstream
类支持磁盘文件输出,如果你需要一个只输出的磁盘文件,可以构造一个ofstream
类的对象,在打开文件之前或之后可以指定ofstream
对象接受二进制或文本模式数据。很多格式化选项和成员函数可以应用于ofstream
对象,包括基类ios
和ostream
的所有功能
如果在构造函数中指定一个文件名,当构造这个文件时该文件是自动打开的。否则,你可以在调用默认构造函数之后使用open
成员函数打开文件,或者在一个由文件指示符表示的打开文件基础上构造一个ofstream
对象
如果仅仅使用预先定义的cout
、cerr
或clog
对象,就不需要构造一个输出流。如果要使用文件流将信息输出到文件,便需要使用构造函数来建立流对象
构造输出流的常用方式如下:
使用默认构造函数,然后调用open
成员函数
ofstream myFile;
myFile.open("");
在调用构造函数时指定文件名
ofstream myFile("");
也可以使用同一个流先后打开不同的文件(在同一时刻只有一个是打开的)
ofstream file;
file.open("FILE1");
//...
file.close();
file.open("FILE2");
file.close();
//当对象file离开它的作用域时就会消亡
c++11标准以前只支持c风格字符数组作为文件名,c++11标准引入string
作为文件名
string filename = "file.txt";
ofstream myFile(filename);
插入运算符用于传送字节到一个输入流对象。插入运算符与预先定义的操纵符一起工作,可以控制输出格式
输出宽度
为了调整输出,可以通过在流中放入setw
操纵符或调用width
成员函数为每个项指定输出宽度,下面的例子在一列中以至少10个字符宽按右对齐输出数值
#include
using namespace std;
int main(){
double values[] = {1.23,35.36,653.2,3456.1};
for(int i=0;i<4;i++){
cout.width(10);
cout << values[i] << endl;
}
return 0;
}
/*
1.23
35.36
653.2
3456.1
*/
从程序的输出结果可以看到,在少于10个字符宽的数值前加入了引导空格
空格是默认的填充符,当输出的数据不能充满指定的宽度时,系统会自动以空格填充,也可以指定用别的字符来填充,使用fill
成员函数可以为已经指定宽度的域设置填充字符的值,如可在上述函数for
前加入代码:
cout.fill('*');
如果说要为同一行中输出的不同数据项分别指定宽度,也可以使用setw
操纵符
#include
#include
#include
using namespace std;
int main(){
double values[] = {1.23,35.36,653.2,3456.1};
string names[] = {"LHQ","CYK","YJQ","JZW"};
for(int i=0;i<4;i++){
cout << setw(6) << names[i] << setw(10) << values[i] << endl;
}
return 0;
}
setw
和width
都不截断数值,仅仅影响紧随其后的域,在一个域输出完后域宽度恢复成它的默认值。但其他流格式选项保持有效直到发生改变,如fill
对齐方式
输出流默认为右对齐文本,为了在上述代码中实现左对齐姓名和右对齐数值,可将程序修改如下
#include
#include
#include
using namespace std;
int main(){
double values[] = {1.23,35.36,653.2,3456.1};
string names[] = {"LHQ","CYK","YJQ","JZW"};
for(int i=0;i<4;i++){
cout << setiosflags(ios_base::left)
<< setw(6) << names[i]
<< rsetiosflags(ios_base::left)
<< setw(10) << values[i] << endl;
}
return 0;
}
这个程序中,通过使用带参数的setiosflags
操纵符来设置左对齐,setiosflags
定义在头文件iomanip
中。参数ios_base::left
时ios_base
的静态常量。这里需要用resetiosflags
操纵符关闭左对齐标志,其影响时持久的
setiosflags
的参数是该流的格式标志值,这个值由如下位掩码指定,并可用位或运算符进行组合
skipws
跳过空白left
左对齐right
右对齐internal
在规定的宽度内,指定前缀符号之后,数值之前,插入指定的填充字符dec
以是十进制形式格式化数值oct
以八进制形式格式化数值hex
以十六进制形式格式化数值showbase
插入前缀符号已表明整数的数制showpoint
对浮点数值显示小数点和尾部的0uppercase
对于十六进制数显示大写字母A
到F
,对于科学格式显示大写字母E
showpos
对于非负数显示正号scientific
以科学形式显示浮点数值fixed
以定点格式显示浮点数值(没有指数部分)unitbuf
在每次插入之后转储并清除缓冲区内容精度
浮点数输出精度的默认值是6,为了改变精度,可使用操作符setprecision
,此外,还有两个标志会改变浮点数的输出格式,即ios_base::fixed ios_base::scientific
,如果设置了fixed
,精度值表示小数点之后的位数,如果设置了scientific
,精度值表示小数点之后的位数
文件输出流成员函数
输出流成员函数有如下3种类型
对于顺序的格式化输出,可以仅使用插入运算符和操纵符,对于随机访问二进制磁盘输出,使用其他成员函数,可以使用或不使用成员函数
输出流的open
函数
要使用一个文件输出流,必须在构造函数或open
函数中把该流与一个特定的磁盘文件关联起来,在这两种情况下,描述文件的参数是相通的
打开一个与输出流关联的文件时,可以指定一个open_mode
标志,可以用按位或运算符组合这些标志,它们作为枚举常量定义在ios_base
类中,例如
ofstream file("filename",ios_base::out | ios_base::binary);
其中第二个表示打开模式的参数具有默认值ios_base::out
,可以省略
下面是一些常用标志
app
:打开一个输出文件用于在文件尾添加数据ate
:打开一个现存文件并查找到结尾in
:打开一个输入文件,对于一个ofstream
文件,使用ios_base::in
作为一个open-mode
可避免删除一个现存文件中现有的内容out
,打开一个文件,用于输出trunc
:打开一个文件,如果它已经存在则删除其中原有的内容,如果指定了out
,但没有指定ate app in
,则隐含为此模式binary
:以二进制模式打开一个文件输出流的close
函数
close
成员函数关闭与一个文件输出流关联的磁盘文件。文件使用完毕后必须将其关闭以完成所有磁盘输出。虽然ofstream
析构函数会自动完成关闭,但如果需要在同一流对象上打开另外的文件,就需要使用close
函数
put
函数
put
函数把一个字符写到输出流中,下面两个语句默认是相通的,但第二个受该流的格式化参量的影响:
cout.put('A');
cout << 'A';
write
函数
write
函数把一个内存中的一块内容写到一个文件输出流中,长度参数指出写的字节数,下面的例子建立一个文件输出流并将Date
结构的二进制值写入文件
#include
using namespace std;
struct Data{
int monday,day,year;
};
int main(){
Date dt = (6,10,92);
ofstream file("data.dat",ios_base::binary)l
file.write(reinterpret_cast<char*>(&dt),sizeof(dt));
file.close();
return 0;
}
write
函数当遇到空字符的时候并不停止,因此能够写入完整的类结构,该函数带有两个参数:char
指针和一个所写的字节数,注意需要用reinterpret_cast
将该对象的地址显式转换成char*
类型
seekp
和tellp
函数
一个文件输出流保存一个内部指针指出下一次写数据的位置,seekp
成员函数设置这个指针,因此可以以随机方式向磁盘文件输出,tellp
成员函数返回该文件位置指针值
错误处理函数
错误处理成员函数的作用是在写到一个流时进行错误处理,各函数及其功能
bad
:如果出现一个不可恢复的错误,则返回一个非0
值fail
:如果出现一个不可恢复的错误或一个预期的条件,例如一个转换错误或者文件未找到,则返回一个非0值,在用零参量调用clear
之后,错误标记被清除eof
:遇到文件结尾条件,则返回一个非0
值clear
:设置内部错误状态,如果用默认参量调用,则清除所有错误为rdstate
:返回当前错误状态!
运算符经过了重载,它与fail
函数执行相同的功能,因此表达式if(!cout)
等价于if(cout.fail())
void*
运算符也是经过重载的,与!
预算符相反,因此表达式if(cout)
等价于if(!cout.fail())
二进制输出文件
最初设计流的目的是用于文本,因此默认的输出模式是文本方式,在不同操作系统中,文本文件的行分隔符不太一样,例如Linux操作系统下的文本文件以一个换行符’\n’作为行分隔符,而Windows操作系统下的文本文件以一个换行符和一个回车符’\r’作为行分隔符。在以文本模式输出时,没输出一个换行符,都会将当前操作系统下的行分隔符写入文件中,这意味着Windows下输出换行符后还会被自动扩充一个回车符,这种自动扩充有时可能出问题
同时,使用二进制输出可以增加效率
#include
using namespace std;
int a[2] = {99,10};
int main(){
ofstream os("test.dat");
os.write(reinterpret_cast<char*>(a),sizeof(a));
return 0;
}
字符串输出流
输出流除了可以用于向屏幕或文件输出信息外,还可以用于生成字符串,这样的流叫做字符串输出流,ostringstream
类就用来表示一个字符串输出流
ostringstream
类有两个构造函数,第一个函数有一个形参,表示流的打开模式,与文件输出流中的第二个参数功能相同,表示打开方式,具有默认值ios_base::out
,通常使用它的默认值,例如可以用下列方式来创建一个字符串输入流
ostringstream os;
第二个构造函数接收两个形参,第一个形参是string
型常对象,用来为这个字符串流的内容设置初始值,第二个形参表示打开模式,与第一种构造函数的形参具有相同的意义
专用于文件操作的open close
函数是ostringstream
类所不具有的
ostringstream
类还有一个特有的函数str
,它返回一个string
对象,表示用该输出流所生成字符串的内容
ostringstream
类的一个典型应用就是将一个数值转化为字符串
#include
#include
#include
using namespace std;
template<class T>
inline string toString(const T&v){
ostringstream os;
os << v;
return os.str();
}
int main(){
string s1 = toString(4);
cout << s1 << endl;
string s2 = toString(1.2);
cout << s2 << endl;
return 0;
}
一个输入流对象是数据流出的源头,3个最重要的输入流类是istream
,ifstream
和istringstream
istream
类最适合用于顺序文本模式输入
ifstream
类支持磁盘文件输入,如果需要一个仅用于输入的磁盘文件,可以构造一个ifstream
类的对象,并且可以指定使用二进制或文本模式,如果在构造函数中指定一个文件名,在构建该对象的时候该文件便自动打开,否则,需要在调用默认构造函数之后用open
函数来打开文件
提取运算符对于所有标准c++数据类型都是预先设计好的,它是从一个输入流对象获取字节最容易的方式
提起运算符用于格式化文本输入,在提取数据时,以空白符为分隔,如果要输入一段包含空白符的文本,用提取运算符就很不方便,在这种情况下,可以选择使用非格式化输入或者成员函数getline
,这样就可以读一个包含有空格的文本块,然后再对其进行分析
定义在ios_base
类中和iomanip
头文件中的操纵符可以应用于输入流。但是只有少数几个操纵符对输入流对象具有实际影响,其中最重要的是进制操纵符dec oct hex
在提取中,hex
操纵符可以接收处理各种输入流格式,例如c C 0xc 0xC 0Xc 0XC
都被解释为十进制数12,任何除0~9 A~F a~f
和X
之外的字符都引起数值变换终止。例如,序列124n5
将变换成数值124
,并且使fail
函数返回true
open
函数如果要使用一个文件输入流,必须在构造函数中的或者open
函数把该流与一个特定磁盘文件关联起来,无论用哪种方式,参数是相通的
当打开与一个输入流关联的文件时,通常要指定一个模式标志。模式标志如下所示,该标志可以用按位或运算符进行组合,用ifstream
打开文件时,模式的默认值是ios_base::in
close
函数close
成员函数关闭与一个文件输入流关联的磁盘文件
虽然ifstream
类的析构函数可以自动关闭文件,但是如果需要使用同一流对象打开另一个文件,则首先要用close
函数关闭当前文件
get
函数非格式化get
函数的功能与提取运算符很相像,主要的不同点是get
函数在读入数据包括空白字符
当输入流结束时,程序读入的值是EOF
getline
函数istream
类具有成员函数getline
,其功能是允许从输入流中读取多个字符,并且允许指定输入终止字符(默认为换行字符),在读取完成后,从读取的内容中删除该终止字符,然而该成员函数只能将输入结果存在于字符数组中,字符数组的大小是不能自动扩展的,造成了使用上的不便,非成员函数getline
可以完成相同的功能,但是可以将结果保存在string
类型的对象中,更加方便,这一函数可以接收两个参数,前两个分别表示输入流和保存结果的string
对象,第三个参数可选,表示终止字符
string line;
getline(cin,line,"\t");
read
函数read
成员函数从一个文件读字节到一个指定的存储器区域,由长度参数确定要读的字节数,如果给出长度参数,当遇到文件结束或者在文本模式中遇到文件结束标记的字符时结束
seekg
和tellg
函数在文件输入流中,保留着一个指向文件中下一个将读取数据的位置的内部指针,可以用seekg
函数来设置这个指针
#include
using namespace std;
int main(){
int values[] = {3,7,5,0,4};
ofstream os("integers",ios_base::out|ios_base::binary);
os.write(reinterpret_cast<char*>(values),sizeof(values));
os.close();
ifstream is("integers",ios_base::in|ios_base::binary);
if(is){
is.seekg(3*sizeof(int));
int v;
is.read(reinterpret_cast<char* >(&v),sizeof(int));
cout << "The 4th integer in the file is:" << v;
}
}
使用seekg
可以实现面对记录的数据管理系统,用固定长度的记录尺寸乘以记录号便得到相对于文件末尾的位置,然后使用get
读这个纪录
tellg
成员函数返回当前文件读指针的位置,这个值是streampos
类型
#include
using namespace std;
int main(){
ifstream file("integers",ios_base::in|ios_base::binary);
if(file){
while(file){
streampos here = file.tellg();
int v;
file.read(reinterpret_cast<char*>(&v),sizeof(int));
if(file && v == 0){
cout << "Position " << here << "is 0" << endl;
}
}
}
file.close();
return 0;
}
字符串输入流提供了与字符串输出流相对应的功能,它可以从一个字符串中读取数据,istringstream
类就用来表示一个字符串输出流
istringstream
类有两个构造函数,最常用的构造函数接收两个参数,分别表示要输入的string
对象和流的打开方式,打开模式默认为in
,可以用下列方式创建一个字符串输出流
string str = ...;
istringstream is(str);
例如,可以把字符串变成数值
template <class T>
T StringToValue(const string &str){
istringstream is(str);
T v;
is>>v;
return v;
}
一个iostream
对象可以是数据的源或母的,有两个重要的I/O
流类都是从iostream
派生的,它们是fstream
和stringstream
,这些类继承了前面描述的istream
和ostream
类的功能
fstream
类支持磁盘文件输入和输出,如果需要在同一个程序中从一个特定磁盘文件读并写到该磁盘文件,可以构建一个fstream
对象,一个fstream
对象是有两个逻辑子流的单个流,两个子流一个用于输入,另一个用于输出
stringstream
类支持面向字符串的输入和输出,可以对同一个字符串的内容交替读写
在编写应用软件时,不仅要保证软件的正确性,而且应该具有容错能力,也就是说,不仅在正确的环境条件下,在用户正确操作时要运行正确,而且在环境条件出现意外或用户使用操作不当的情况下,也应该有正确合理的处理方法,不能轻易出现死机,更不能出现灾难性的后果,由于环境条件和用户操作的正确性是没有保障的,所以我们在设计程序时,就要充分考虑到各种意外情况,并继续恰当的处理,这就是异常处理
若频繁检测条件,如下
执行任务
如果任务执行不正确
执行错误处理
执行下一项任务
如果任务执行不正确
执行错误处理
上面的伪代码中,没执行以下任务就要检测成功与否,使得程序逻辑与错误处理逻辑混在一起,使得程序的可读性、可修改性、可维护性差,难以调试
异常处理技术使我们可以将错误处理代码从主逻辑中移出,使程序更清晰,能增强程序的可维护性
同时,我们可以有选择地处理异常:所有异常、某种特定类型的所有异常、一组相关类型的所有异常
抛掷异常的程序段
...
throw 表达式;
...
捕获并处理异常的程序段
try
复合语句
catch(异常声明)
复合语句
catch(异常声明)
复合语句
若有异常则通过throw
创建一个异常对象并抛掷
将可能抛出异常的程序段嵌在try
块之中,通过正常的顺序执行到达try
语句,然后执行try
块后的最后一个catch
子句后面的语句继续执行
catch
子句按其在try
块后出现的顺序被检查,匹配的catch
子句将捕获并处理异常或继续抛掷异常,或部分处理然后继续抛掷
如果匹配的处理器未找到,则库函数terminate
将被自动调用,其默认是调用abort
终止程序
可以在函数的声明中列出这个函数可能抛掷的所有异常类型
如:void fun() throw(A,B,C,D);
若无异常接口声明,则此函数可以抛掷任何类型的异常
不抛掷任何类型异常的函数声明如下
void fun() throw();
void fun() noexcept;
尽量避免使用异常声明的情况有三种
自动的析构
catch
异常处理后
try
块开始到异常被抛掷处之间的构造(且尚未析构)的所有自动对象进行析构catch
处理之后开始恢复执行exception
标准程序库异常类的公共基类logic_error
表示可以在程序中被预先检测到的异常runtime_error
表示难以被预先检测到的异常#include
#include
#include
using namespace std;
double area(double a,double b,double c) throw(invalid_argument){
if(a <= 0 || b <= 0 || c <= 0)
throw invalid_argument("the side length should be positive");
if(a + b <= c || b + c <= a || c + a <= b)
throw invalid_argument("the side length should fit the triangle inequation");
double s = (a + b + c) / 2;
return sqrt(s * (s - a) * (s - b) * (s - c));
}
int main(){
double a,b,c;
cin >> a >> b >> c;
try{
double s = area(a,b,c);
cout << "Area:" << s;
}catch(exception &e){
cout << "Error:" << e.what() << endl;
}
return 0;
}
stack unwind
,二进制代码文件的大小会比较大