C++基础知识

 

预备知识

1,源代码知识

 

给C++源文件命名时要使用正确的后缀,后缀一般由一个句点和多个字符组成,这些字符被称作扩展名

 

  • UNIX:C、cc、cxx、c(UNIX是区分大小写的,C和c这两个扩展名都有效,实际上标准C才使用c,C++使用C)
  • GNU C++:C、cc、cxx、cpp、c++
  • Microsoft Visual C++:cpp、cxx、cc

 

2,编译与链接

 

  • UNIX:用CC命令来编译C++程序,cc命令用来编译标准C程序,例如要编译C++源代码文件spiffy.C,应输入以下命令

 

CC spiffy.C

若程序没有错误,将会生成一个扩展名为o的目标代码文件,在上述例子中,生成的文件为spiffy.o,接下来编译器将目标代码文件传递给系统链接程序,该程序将目标代码与库代码结合起来,生成一个可执行文件a.out。如果只有一个源文件,则spiffy.o会被删除。

如果编译一个新程序,上一次生成的a.out会被覆盖,因为a.out会占据大量的空间。如果需要保留a.out文件则需要更改其文件名

编译多个源文件时,应使用如下命令

CC my.C precious.C

包含多个源文件时,编译器不会删除目标代码文件,如果只修改了my.C文件,则可以使用如下命令编译

CC my.C precious.o
  • LINUX:与UNIX中类似

编译一个源文件时

g++ spiffy.cxx

同样是生成一个可执行文件a.out

编译多个源文件时

g++ my.cxx precious.cxx

如果只修改了my.cxx,则使用如下命令重新编译

g++ my.cxx precious.o
  • Windows:命令的使用方式与上述二者类似

例如编译名为great.cpp的文件时

g++ great.cpp

生成可执行文件a.exe

3,程序出错时,应先从第一个出错的地方进行修改,因为第一个错误可能导致了后面的错误

4,有的IDE会在程序运行结束后自动关闭窗口,可以使用cin.get()来避免这个问题,cin.get()用于读取键击

开始学习C++

1,int main()=int main(void)

2,应避免使用void main(),这不是C++的一个标准格式

3,main()函数末尾中如果没有返回语句时,编译器会默认它以return 0结尾,但这个只对main()函数适用

4,头文件命名

 

头文件类型约定示例说明
C++旧式风格以.h结尾iostream.hC++程序可以使用
C旧式风格以.h结尾math.hC、C++可以使用
C++新式风格没有扩展名iostreamC++程序可以使用,使用namespac std
转换后的C加上c前缀,没有扩展名cmathC++程序可以使用

5,函数原型和函数定义

函数原型只描述函数的接口,包括了参数类型和返回值的类型,函数定义中包含了函数的代码。函数原型在头文件中,函数定义在库文件中,一个函数定义不能嵌套在另一个函数定义中。

6,对于命名空间一个通行的理念,只让用到该命名空间的函数访问该命名空间。以命名空间std为例,下面是四种访问命名空间的方法

 

  • 将using namespace std放在所有函数定义之前,所有函数都可以使用std中的元素
  • 将using namespace std放在特定的函数中,只有该函数可以使用std的元素
  • 在特定函数中使用如using std::cout这样的指令,让该函数只可以使用cout这个元素
  • 不使用using这个指令,在用到命名空间中的元素时,采用std::cout这种形式

处理数据

1,宽度(width)用于描述存储整数时使用的内存的量,C++中的基本整型有char,short,int,long,long long(C++11新增),每种类型都有有符号版本和无符号版本,所以共有10种。

 

  • short至少16位
  • int至少与short一样长
  • long至少32位,且至少与int一样长
  • long long至少64位,且至少与long一样长

 

8 bit=1 byte

2,初始化变量的方法

int x=1
int x(1)
int x{1}
int x={1}
int x{}
int x={}

前两种比较常用,最后两种当大括号为空时,变量被初始化为0

3,头文件climits定义了符号常量,定义了数据类型的最大值和最小值

4,unsigned是unsigned int的缩写

5,选择整型类型时通常根据该整数的大小来确定,如果需要表示的整数有可能大于16位整数值时,就应该选用long类型来表示,即使int类型为32位,这样是为了防止程序移植到16位的系统时发生错误。

6,C++采用前两位来表示数字常量的基数

 

  • 第一位为1-9,则基数为10
  • 第一位为0,第二位为1-7,则基数为8
  • 以0x或0X开头的,则基数为16

7,在默认情况下,C++以十进制格式显示整数,如果想以八进制或者十六进制显示整数,则应该使用控制符oct,hex

  • binary 二进制
  • octal 八进制
  • decimal 十进制
  • hexadecimal 十六进制
int num=42;
cout<<num<<endl;//十进制
cout<<oct;
cout<<num<<endl;//八进制
cout<<hex;
cout<<num<<endl;//十六进制

cout<<hex和cout<<oct这两行代码不会在屏幕上显示任何内容

8,C++默认将常量存储为int型,除非有以下两种情况

 

  • 该值太大,不能存储为int型
    十进制int,long,long long
    八进制或者十六进制int,unsigned int,long,unsigned long,long long,unsigned long long
    八进制和十六进制常用来表示内存地址,内存地址没有符号,所以无符号版本比有符号版本更加适合表示八进制数和十六进制数

该值用了特殊的后缀来表示其类型

后缀说明
u,Uunsigned int
l,Llong
ll,LLlong long
ul,UL,lu,LUunsigned long
ull,ULL,uLL,Ullunsigned long long

 

9,任何非零值为true,零为false

10,const限定符用于声明常量,作用于#define类似,在声明const时就应该对其进行初始化,例如:

const int num=10;

如果不在声明时进行初始化,则该常量的值是不确定的,且无法修改,不应该采用如下这种形式:

const int num;
num=10;

11,C++有两种书写浮点数的方法

 

  • 小数点表示法,例如12.34,1.95,8.0
  • E表示法,比如3.45E6,表示的是3.45与1000000相乘的结果,E6表示的是10的6次方,6被称为指数,3.45被称为尾数(可以用E也可以用e,指数可以是正数也可以是负数);2.52e+8、8.33E-4、7E5、-1.32e13这些形式都可以;-8.33E4表示-83300,前面的符号用于表示数值的正负,指数的符号用于缩放

12,C++中的浮点类型有三种float,double,long double

 

  • float的有效位数至少为32位
  • double至少为48位,且不少于float
  • long double可以为80,96,128位,且不少于double

这三种类型的指数范围至少为-37到37

13,浮点常量通常存储为double类型,若想以float类型存储则需要在浮点常量后面加上f或者F后缀,对于long double类型,则应使用l或者L后缀。

14,整数和浮点型统称为算术类型

15,类型转换,C++允许将一种变量类型的值赋给另外一种变量类型,这样做时,值的类型会转变为接受变量的类型,比如:

float num1;
double num2;
num2=num1;

此时,程序会将num1的值扩展为double,得到一个新的值,将该值赋给num2,num1的值不变;通常将一个精度较低的类型转变为一个精度较高的类型不会出现问题,但反过来就有可能出现问题,可能会导致精度降低的问题,比如一个long类型的数2111222333,假如将它赋给float,因为float的有效数字只有6位,因此该值会被四舍五入为2.11122E9

16,C++11中使用大括号初始化的方式称为列表初始化,它对类型转换的要求严格,它不允许缩窄(narrowing),例如float类型的值不能赋给int类型,在不同整型之间的转换或者整型转换为浮点型可能被允许,条件是编译知道目标变量能够正确地存储赋给它的值。

17,C++进行算术运算时的类型转换

 

  • 如果有一个操作数的类型为long double,则将另外一个操作数的类型转换为long double
  • 否则,如果有一个操作数的类型为double,则将另一个操作数的类型转换为double
  • 否则,如果有一个操作数的类型为float,则将另一个操作数的类型转换为float
  • 否则,说明操作数都为整型,执行整型提升
  • 如果两个操作数都为有符号或无符号,则将级别较低的操作数转换为级别较高的操作数
  • 如果一个操作数为有符号,另一个操作数无符号,无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数的类型
  • 否则,如果有符号操作数的类型可以表示无符号操作数的所有取值,则将无符号操作数转换为有符号操作数的类型。
  • 否则,将两个操作数都转换为有符号类型的无符号版本

 

18,C++11中的auto声明,它可以将变量的类型设置为与初始值相同,例如:

auto num1=1.2;//num1为double
auto num2=100;//num2为int

复合类型

1,C++数组的元素下标一定从0开始

2,如果没有初始化数组,数组中元素的值是不确定的,为以前驻留在该内存单元中的值

3,只有在定义数组时可以使用初始化,不能将一个数组赋给另一个数组。

4,如果只对数组中的一部分元素进行初始化,则编译器会将其他元素设置为0,因此要想把一个数组的全部值设为0的话,只需要将第一个元素的值设为0即可。

5,在初始化数组时,如果[]内内容为空,形如typename arrayname[],则编译器会计算元素的个数,例如

int array1[]={1,1,1}

该数组包含3个元素

6,C++11中新增了一些初始化数组的方式,称为列表初始化

int array1[4] {1,2,3,4};
int array2[4]={1,2,3,4};
int array3[4] {};
int array4[4]={};

如果大括号中不包含任何东西,则把所有元素初始化为0;列表初始化禁止缩窄转换

7,C++处理字符串的方式有两种,一种叫C风格字符串,一种是基于string类库的方法

 

  • C风格字符串要以空字符结尾,即'\0',ASCII码为0,C++很多处理字符串的函数都是逐个处理字符,直到遇到空字符为止;在计算存储字符串数组的大小时,不能忘记将空字符计算在内
char string1[3]={'d','o','g'};
char string2[3]={'o','k','\0'};
  • 上述第一个数组不能被称作字符串,只是一个字符数组;第二个字符数组可以被称作字符串;不应该将不是字符串的字符数组当成字符串来处理
char string1[10] = "sleep"; 
char string2[] = "sleep";

 

  • 上述是两种简便将字符数组转变为字符串的方法,这种方法隐式地包含了结尾的空字符
  • 字符常量(使用单引号)与字符串常量(使用双引号)不能互换,例如'S'在ASCII码系统上只是83的简写表示,"S"不是字符常量,它是由字符S和\0组成的字符串,"S"实际表示的是字符串的内存地址
  • strlen()函数返回的是字符串的长度,而不是数组本身的长度,即它并不包括空字符,例如:如果sleep为一个字符串,则存储它的数组的大小为strlen(sleep)+1
  • getline()和get()这两个函数可以读取一行输入,直到到达换行符,getline()会将换行符丢弃,而get()会保留换行符;getline()有两个参数,第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数,如果该参数为20,则最多读取19个字符,余下的空间用于添加结尾的空字符;它会读取用户的输入,包括结尾的换行符,然后将换行符替换为空字符。get()函数不会丢弃换行符,它与getline()解释参数,接受参数的方式相同,不过由于其不会丢弃换行符,在连续调用时,例如以下代码,第二次调用看到的第一个字符便是换行符,会让它误以为已到达行尾,因此我们要借助其他方式让它跨过这个换行符,不带参数的cin.get()可以读取下一个字符,因此可以用它来处理换行符。
cin.get(name,10);
cin.get(id_num,10);
cin.get(name,10);
cin.get();
cin.get(id_num,10);
  • string类库使得不再需要用字符数组的方式来存储字符串,string类定义隐藏了字符串的数组特性,可以想处理普通变量那样处理字符串,string对象的使用方式跟C风格字符串相似,可以用跟C风格字符串的方式初始化字符串,可以通过cin的方法输入元素到字符串,可以用过cout的方式输出字符串,可以通过字符数组下标的方式访问字符串
  • string对象与字符数组的区别是,我们可以将string对象声明为一个简单变量而不是一个数组,例如下面的代码,第二行代码创建了一个空的string对象,类的设计使得系统可以自动处理string的大小,string对象的使用更方便,也更安全;char数组实际上是一个存储字符串的char存储单元,而string类变量是一个字符串实体
string name = "lauv";
string id_num;
cin>>id_num;

 

  • string类使得字符串的赋值和拼接更加简单,使用字符数组之间不能赋值,而string类对象之间可以赋值和更加便捷地拼接
str1 = str2;
str3 = str1 + str2;
  • 在没用string类之前,可以使用C语言库中的函数来完成字符串之间的操作
strcpy(str1,str2);//将str2的内容复制到str1中
strcat(str1,str2);//将str2拼接到str1后
  • str.size()可以查看str字符串的长度,跟C风格的字符串一样,它返回的是字符串本身的长度
  • string对象与字符数组对getline()的使用方法不一样,因为string对象是新增的,istream对象是很久之前就存在了,设计istream时考虑到了int,double等基本数据类型,所以它含有处理基本数据类型的方法,但是没有处理string类对象的方法
    string str1;
    char str2[10];
    getline(cin,str1);
    cin.getline(str2,10);
  • 原始字符串,在原始字符串中字符表示的就是自己,比如\n不再表示换行符,而是表示一个斜杠和一个n,在屏幕上显示是就直接显示这两个字符,又比如想使用引号时,不再需要使用\",原始字符串使用前缀R和"(和)"作为界定符,如下代码,\n和\t都不会变为换行符和制表符,而是直接显示它们的字符;假如在原始字符串中需要使用"(和")这两个符号的话,可以在原始字符串的界定符号"(和)"做如下修改,比如"+*(和)+*"
    cout<<R"(Hello \n  \t World)";

8,结构体可以采用外部声明和内部声明两种方式,内部声明的结构体只可以在某个函数中使用,外部声明的结构体可以在整个程序中使用

  • C++11中也支持采用列表初始化的方式初始化结构体的成员,比如有一个结构体student
    student stu1 {"lauv",10,10};
    student stu2 {};//各个成员都会被设置为0,若成员中有数组,则数组中的各个元素都会被置为0
  • 可以将定义结构和创建结构变量的工作同时完成,还可以定义没有名称的结构体,如果需要定义没有名称的结构体,则需要在结构体后同时创建一个成员变量
  • struct student
    {
    char name[10];
    int id_num;
    }stu1,stu2;
    struct 
    {
    int x;
    int y;
    }position;
  • 同一个结构体的不同成员之间可以相互赋值
    stu1 = stu2;

9,共用体(union),它能够存储不同的数据类型,但只能同时存储其中一种数据类型,共用体一般用于节省空间,比如下面的例子,共用体id既可以是long,又可以是int,还可以是字符数组,由于它每次只能存储一个值,必须确保有足够的空间存储数据,共用体的长度为其最大成员的长度

union id
{
long id_num;
int num1;
char id_char[20];
}id1;

10,枚举(enum)用于创建符号常量,spectrum被称作一个枚举,red,orange等常量叫做枚举量,默认情况下枚举量对应整数值0到7,比如red为0,orange为1,以此类推,对于枚举只定义了赋值运算符,在不使用强制类型转换的情况下,只能将定义枚举时使用的枚举量赋给这个枚举的变量

enum spectrum {red,orange,yellow,green,blue,violet,indigo,ultraviolet};
spectrum band;
band = blue;

11,枚举量是整型,它可以被提升为int类型,但int类型不能自动转换为整型,

int color = blue;
color = 3 + red;
band = spectrum(3);//如果一个int值对于一个枚举是有效的,则可以通过强制类型转换将它赋给枚举变量

12,如果只打算使用枚举类型的常量而不创建枚举变量,则可以省略枚举类型中的名称

enum {red,orange,yellow,green,blue,violet,indigo,ultraviolet};

13,可以使用赋值运算符显式地设置枚举量的值,指定的值必须是整数,也可以只定义一部分枚举量的,可以创建多个值相同的枚举量

enum bits{one = 1,two = 2,four = 4,eight = 8};
enum bitstep{first,second = 100,third};//这里first的值默认为0,third的值为101,没有被初始化的枚举量的值会比前面的枚举量的值大一

14,枚举的取值范围,分为上限与下限,寻找上限时,先找到枚举量的最大值,然后找到大于这个最大值的,最小的2的幂,将它减1,得到的就是这个枚举取值的上限,比如一个枚举中,枚举量的最大值为101,在2的幂之中,比这个数大的最小值为128,减1得到127,所以该枚举取值的上限为127。计算下限时,找到枚举量的最小值,若不小于0,则枚举量的最小值为0,若枚举量的最小值小于0,则采取跟找上限一样的办法,不同的是要在前面加上负号,例如最小的枚举值为-6,比它小的最大的2的幂为-8,因此它的下限为-7

15,指针是一种变量,它存储的是值的地址而不是值本身,平时可以用&运算符来找到常规变量的地址,地址一般用十六进制表示。在使用常规变量时,我们将值视为指定的量,地址视为派生量;在使用指针时,将地址视为指定的量,将值视为派生量,指针名表示的是地址,将*运算符运用于指针,则可以得到该地址处存储的值

int * pointer;//定义一个int指针
int num = 4;
pointer = &num;//将num值的地址赋给指针
  • num与指针pointer相当于一枚硬币的两面,num与*pointer等价,可以像使用int变量那样来使用*pointer

16,声明指针时,要说明指针指向哪种类型的值,创建指针时,对于每一个指针都要有一个*号

int* p1,*p2;//创建两个p1,p2
int* p1,p2;//创建一个指针p1和一个int变量p2

17,C++在创建指针时,只会分配用来存储地址的内存,而不会分配指针所指向的数据内存,在对指针使用*运算符之前,一定要将指针初始化为一个确定的,适当的地址

18,运用指针和运算符new可以动态分配内存,new分配的内存只能通过指针来访问它,跟常规定义的变量通过名称访问的方式不同

int* num_pointer = new int;

19,new分配的内存块与常规变量声明分配的内存块不相同,new从被称为堆(heap)和自由存储区(free store)的内存区域中分配内存,常规声明的变量存储在称为栈(stack)的内存区域中

20,使用new来分配内存,delete用于释放内存,delete时在delete后面加上指向内存块的指针,new和delete一定要配对使用,否则将发生内存泄漏。delete释放的是指针所指向的内存,但不会删除指针,可以将该指针重新指向其他内存块,delete只能用于释放new分配的内存。一般来说,只用一个指针指向一个内存块,这样可以防止重复释放同一块内存

int* num_pointer = new int;
delete num_pointer;

21,new也可以用来创建动态数组,创建时需要声明数组的类型和元素个数,new运算符会返回第一个元素的地址

int* pointer = new int [10];

其释放动态数组的delete的格式也有所不同

delete [] pointer;//[]告诉程序要释放的是整一个数组

22,在使用new和delete时应该遵循以下规则

  • 不要使用delete来释放不是new分配的内存
  • 不要用delete释放同一块内存两次
  • 如果使用new[]为数组分配内存,应使用delete[]释放内存
  • 如果使用new为实体分配内存,则应使用delete释放内存
  • 对一个空指针使用delete是安全的

23,使用动态数组时,我们可以将创建动态数组时的指针当做数组名使用即可,

int* pointer = new int[10];
pointer[0] = 1;//访问数组中的第一个元素
pointer[1] = 2;//访问数组中的第二个元素

24,在多数情况下,C++会将数组名解释为数组中的第一个元素的地址;在很多情况下,可以相同的方式使用数组名和指针名,区别是数组名是一个常量,而指针的值可以修改;一个区别是,当对一个数组使用sizeof运算符时得到的是一个数组的长度,而对指针运用sizeof运算符时得到的是指针的长度。

25,对数组取地址时,数组名不会被解释为其地址,数组名表示的是数组中第一个元素的地址,对数组名运用&地址运算符时,得到的是整个数组的地址

short tell[10];
cout<<tell<<endl;//输出数组中第一个元素的地址
cout<<&tell<<endl;
short (*pas)[20] = &tell;

26,使用数组声明来创建数组时,属于静态联编,数组的长度在编译时就已经确定,不能修改;采用new运算符创建动态数组属于动态联编,数组的长度在运行时确定,也是在运行时才为数组分配空间

27,数字组名是数组中第一个元素的地址,因此下面代码中flower为"r"元素的地址,cout对象认为char的地址是字符串的地址,因此它将打印该地址处的字符,接着输出后面的字符,直到遇到空字符为止;如果给cout提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止;因为flower是一个char的地址,所以我们也可以将指向一个char地址的指针作为cout的参数,为了让cout对字符串的输出保持一致,C++对于双引号的字符串也是一个地址,它是字符串中第一个字符的地址,在输出时,程序不会将整个字符串发送给cout,而是发送字符串的地址。因此用字符数组描述的字符串,用引号描述的字符串还有指针描述的字符串,它们的处理方式是一样的,都是传递地址,这种方式比逐个处理字符串要高效

char flower[10] = "rose";
cout<<flower<<"s are red\n";

28,new运算符也可以用于创建动态结构,创建动态结构时不能将成员运算符.用于结构名,因为动态创建的结构没有名称,只知道它的地址,这时应该采用箭头成员运算符(->);简单来说,当涉及指向结构的指针时,用->;当涉及结构成员的名称时,用.

struct things
{
int good;
int bad;
};
things good = {4,453};
things* p1 = &good;
//此时p1->good为4,p1->bad为453,p1是指向结构的指针,则(*p1)是结构本身,因此(*p1).good是结构的good成员,(*p1).good跟p1->等价

29,C++中不保证新释放的内存就是下一次new使用的内存(因为分配内存时要寻找合适的内存空间,上一次分配时这块内存空间合适并不代表着下一次这块内存空间也合适?)

30,C++有3种管理内存的方法:自动存储,静态存储,动态存储(又叫堆heap或者自由存储区free storage),C++11中新增了线程存储

 

  • 在函数内部定义的常规变量使用自动存储空间,它们被称为自动变量,它们在所属的函数运行时产生,在函数运行完成后消亡,自动变量实际上是局部变量,它的作用域为包含它的代码块;自动变量存储在栈中,栈采用后进先出的方式,程序执行的过程中,栈会不断地增大和缩小
  • 静态存储是在整个程序执行期间都会存在的存储方式
  • 动态存储使得数据的生命周期不完全受程序和函数的运行时间控制,动态存储的数据位于堆(自由存储区中)。由于自动分配和自动回收,栈中占用的内存空间总是连续的,而new和delete的影响使得自由存储区中占用的内存空间不一定是连续的,增加了内存管理的难度

31,类型组合

数组,结构和指针可以用各种方式将他们组合起来,例如结构数组,指向这种结构的指针,创建指针数组

struct interesting_year
{
int year;
}
interesting_year year[3];
interesting_year.year[0]=2000;
interesting_year.year[1]=2001;
interesting_year.year[2]=2002;
interesting_year *p=year;//指针指向year[0]
const interesting_year * p1[3]={&year[0],&year[1],&year[2]};
cout << p1[0]->year<<" "<<p1[1]->year << " " << p1[2]->year << endl;
cout << ((*p1[0]).year) << " " << ((*p1[1]).year) << " " << ((*p1[2]).year) << endl;
//const interesting_year ** p2 = p1;
auto p3 = p1;
cout << (*(p3+1))->year << endl;
interesting_year y1, y2, y3;
y1.year = 2000;
y2.year = 2001;
y3.year = 2002;
interesting_year * p4[3] = { &y1, &y2, &y3 };
auto  p5 = p4;
cout << (*(p5+1))->year;

上述代码中被注释掉的部分,书本上写的是可以编译通过,但是在VS2013上却无法编译通过,用到指向指针的指针时,要注意代码的格式,该用括号的地方要用括号括起来

32,模板类vector和array是动态数组的替代品

  • 使用vector时要包含头文件<vector>,vector包含在命名空间std中,vector也使用new和delete来实现动态存储,不过这种工作是自动完成的
#include <vector>
using namespace std;
//using std::vector
  • 声明vector的对象时,可以说明对象包含数据的类型,数据的个数
vector <int> my_int;
vector <double> my_double{ 10 };
  • vector类的功能比数组强,但是效率更低,如果需要长度固定的数组,数组是更好的选择,不过代价是使用起来不那么方便,不那么安全。所以C++11中新增了模板类array,array是固定长度的,跟数组一样使用栈来存储数据,但是它比数组更方便,更安全,跟vector不同的是,在声明array类对象时需要确定其元素的个数

循环与关系表达式

1,cout通常在显示bool型变量的值时会将它们转变为0或者1,可以使用如下代码来使其显示false或true

cout.setf(ios_base::boolalpha);

2,自增运算符和自减运算符有前缀和后缀两个版本,自增和自减运算符是一种漂亮简约的方法,不过使用时一定要谨慎

int x=5;
int y=6;
x=y++;//先将y的值赋给x,然后y再执行++
x=++y;//y的值先++,再将y的值赋给x
int x;
x++;
++x;
x--;
--x;

选择使用前缀格式还是后缀格式对程序的行为没有影响,但执行速度可能会有细微的差别。对于内置的类型,采用哪种格式不会有差别,但对于用户定义的类型,如果有用户定义的自增和自减,则前缀的效率比后缀的高

3,递增递减运算符可以与指针结合使用,前缀递增、前缀递减与解除引用运算符(*)的优先级相同,以从右到左的方式进行结合。后缀递增与后缀递减的优先级相同,但是它们比前缀运算符的优先级高,以从左到右的方式进行结合

4,复合语句有一个特性,如果在语句块中定义一个新的变量,则仅当程序执行该语句块中的语句时,该变量才存在,语句块执行完之后,变量将被释放。如果在一个代码块外定义了一个变量,在代码块中再次声明这个变量的话,在声明位置处到语句块结束的位置,新变量将隐藏旧变量,语句块结束后,旧变量再次可见

5,编写延时循环。C++中的clock()函数有助于完成这个工作,这个函数返回程序开始执行后所用的系统时间,不过这个它返回的时间的单位不一定是秒,其次它的返回类型在一些系统上可能是long,在别的系统系统上可能是unsigned long或者其他类型。在头文件ctime中定义了一个符号常量CLOCK_PER_SEC,该常量为每秒钟包含的系统时间单位数。因此,将clock()返回的时间除以CLOCK_PER_SEC可以得到秒数,将CLOCK_PER_SEC乘以秒数,可以得到以系统时间为单位为单位的时间。clock_t为clock()返回的时间的类型的别名,因此可以将变量声明为clock_t类型

float secs;
cin >> secs;
clock_t delay = secs*CLOCKS_PER_SEC;
clock_t start = clock();
while (clock() - start < delay);
cout << "Finish!" << endl;

上面这段代码演示了可以根据用户输入的秒数,延时输出后面的内容

6,C++创建类型别名的方式有两种

  • 一种是使用预处理器
#define BYTE char//BYTE为char的别名
  • 一种是使用C++关键字 typedef
typedef char byte//byte为char的别名

相比之下,typedef是一种更好的办法

7,二维数组,比如int[x][y],可以看作该数组有x个元素,每个元素中又包含有y个整数,可以被一系列由逗号分隔的一维数组来初始化。

8,许多程序都逐字节地读取文本输入或文本文件,istream类提供了多种完成这种工作的方法,如果 ch是一个char变量,则cin>>会将输入的下一个字符读入到ch中,不过它会忽略空格、换行符和制表符。cin.get(ch)会读取输入的下一个字符并将其存储到ch中,成员函数cin.get()返回下一个输入的字符,包括空格,制表符和换行符,因此可以这样使用它,ch=cin.get()

9,for和while是入口条件循环,do...while...是出口条件循环

10,字符函数库cctype中包含有大量的与字符相关的函数,这些函数的返回类型是int而不是bool,但通过bool转换,通常可以将它们视为bool

函数名称返回值
isalnum()如果参数是字母或者数字,则返回true
isalpha()如果参数是字母,则返回true
iscntrl()如果参数是控制字符,则返回true
isdigit()

如果参数是数字(0~9),则返回true

isgraph()如果参数是除空格之外的打印字符,则返回true
islower()如果参数是小写字母,则返回true
isprint()如果参数是打印字符(包括空格),则返回true
ispunct()如果参数是标点符号,则返回true
isupper()如果参数是大写字母,则返回true
isxdigit()如果参数是十六进制数字,0~9,a~f或者A~F的话,则返回true
tolower()如果参数是大写字母,则返回其小写,否则返回原来的参数
toupper()如果参数是小写字母,则返回其大写,否则返回原来的参数

11,?:是C++中唯一一个需要3个操作数的运算符,常被用来替代if...else...

(x<2)?!x?x:y:z

上述的表达式可能有点难以理解,不过只需要将其按正常格式分开即可,其实顺序为先判断x小于2,,若为真,则判断!x是否为真,若!x为真,则x,若!x为假,则y;若x小于2为假,则z

12,switch()语句根据标签来选择执行的语句,通常标签为int或char常量,还可以为枚举量,如果没有匹配的语句,则会执行default标签内的语句,而且default标签是可选的,没有匹配的语句又没有default标签时,程序会转到switch后面的语句执行。每一条标签的语句中,可以插入break语句,break语句可以使程序调到switch后面的语句执行;如果没有break语句,那么程序会继续执行switch中的其他语句,知道执行完为止,或者遇到break

13,在一些情况下,switch和if...else...是可以互换的,switch中的case标签只可以是常量,只能是char,int或者枚举量;当所有选项都可以用整数常量来表示时,则switch和if...else...都可以使用,当选项数量不少于3个时,在这种情况下应该使用switch,switch的效率会更高

14,break和continue都可以用于循环语句之中,break可以使程序跳出循环,continue可以使程序跳过余下的代码,重新开始新一轮的循环

15,简单的文件IO,通过cin进行输入时,程序会将输入视为一系列的字节,这些字节都被解释成字符编码,不管目标数据类型是什么,cin再对其进行处理。

写入到文本文件中

  • 需要包含fstream头文件
  • fstream头文件中包含了ofstream类
  • 创建一个或多个ofstream对象,由自己命名,只要符合命名规范即可
  • 指定命名空间(using namespace std;using std::XX)
  • 将文件与ofstream对象绑定起来,通常使用open()方法
  • 使用完文件之后,使用close()方法关闭连接
  • 结合ofstream对象和<<运算符来输出各种不同类型的数据

读取到文本文件中,与写入到文本文件中类似

  • 必须包含头文件fstream
  • 头文件fstream中包含了ifstream类,由自己来命名
  • 指定命名空间
  • 将文件与ifstream对象关联起来,通常使用open(“XXX”)方法
  • 使用完文件之后要用close()方法关闭文件
  • 结合ifstream对象和>>来读取文件中的内容
  • 可以使用ifstream对象与get()来读取一个字符,采用getline()方法来读取一行字符
  • 结合ifstream对象与eof(),fail(),good()等方法来判断读取是否成功
  • 当ifstream对象本身被用作测试条件时,当最后一个读取操作成功,它将转换成true,否则为false
#include <iostream>
#include<string>
#include <fstream>
int main()
{
	using namespace std;
	ofstream fobj1;
	fobj1.open("liugy.txt");
	fobj1 << "nice luke";
	fobj1.close();
	ifstream fobj2;
	fobj2.open("liugy.txt");
	char str[10];
	while (fobj2.good())
	{
		fobj2.getline(str, 10);
	}
	cout << str;
	fobj2.close();
	cin.get();
}

上述代码是写入文本文件和读取文本文件内容的示例

函数

1,函数原型

函数原型提供了函数到编译器的一个接口,函数原型可以告诉编译器,调用该函数时需要提供什么类型的参数,需要提供几个参数,如果程序没有提供正确的参数,编译器会捕获这种错误;函数会将返回值放到CPU寄存器或者内存之中,编译器会根据函数原型中提供的返回值的类型确定如何应该检索多少个字节和如何解释它们,避免使用函数原型的方式就是在第一次使用它之前定义它

  • 函数原型使编译器正确地处理函数的返回值
  • 函数原型使编译器正确地获取参数的类型
  • 函数原型使编译器正确地获取参数的个数

2,程序传递给函数的值叫做实参,函数中用于接收实参的值叫做形参,在函数中对形参进行的操作不会影响实参,即传递常规变量时,函数会使用原数据的拷贝;在函数中使用数组时,一般采用指针的方式传递数组的首地址以及数组元素的个数,它使用的是原始数据,不会进行数据的拷贝,这样可以节省大量拷贝数据时需要的时间和系统开销,不过有可能会误修改数据,带来损坏数据的风险,这种情况下可以使用const关键字来防止意外地修改数据

void test(const int arr1[],int n);

3,在大多数情况下,数组名可以解释为数组中第一个元素的地址,但该规则也有一些例外

  • 在数组声明中,使用数组名来标记数组存储的位置
  • 对数组名用sizeof将得到整个数组的长度
  • 对数组用&地址运算符将会得到整个数组的地址
  • 有两个恒等式务必记住
array[i]==*(array+i);//数组中对应元素的值
&array[i]==array+i;//数组中对应元素的地址

4,C++传统的处理数组的函数一般都需要将指向数组起始位置的一个指针和数组长度作为参数传给函数,利用这两个参数,函数可以找到所需要的全部数据;另外一种方法是使用数组区间,即将两个指针作为参数,一个指针指向数组的起始位置,另外一个指针指向数组的最后一个元素的后面

int arr[10];

假设有一个这样的数组,则数组区间arr,arr+10定义了该数组,数组名arr指的是arr[0],arr+9指的是数组中的最后一个元素arr[9],arr+10指的是arr[10],即数组结尾后面的一个位置

5,可以将const关键字用于指针,这两种用法的const的位置是不一样的

  • 将指针指向一个常量对象,不能通过指针去修改它的值
  • 将指针本身声明为一个常量,不能修改指针指向的位置
const int * p1=&num1;//指针指向常量对象
int * const p2=&num2;//指针本身声明为常量

6,尽可能使用const关键字,将指针参数声明为指向常量数据的指针

  • 可以防止由于修改数据导致的编程错误
  • 使得函数可以接受const和非const实参,否则只可以接受非const数据

7,函数与二维数组,二维数组作为参数时,其函数原型可以为以下两种形式

void test(int (*p)[4],int size);//size指的是行的数量,4为列的数量
void test1(int arr[][3],int size);//3是列的数量,size为行的数量,它作为一个参数传进来

函数的参数不能是数组,第一种写法中的括号不能缺少,因为它是一个由4个指向int的指针构成的数组,而不是一个指向有四个int的数组的指针;第二种写法跟一维数组的写法类似

8,将字符串作为函数的参数时,意味着传递的是地址,可以使用const关键字来防止对字符串的错误修改 。

9,将C风格字符串作为函数的参数时,有以下三种方式

  • 字符数组
  • 用引号括起来的字符串常量
  • 被设置为字符串地址的char指针

这三种类型都为char指针,所以可以将char指针作为函数的参数;C风格字符串都是以空字符结尾的,因此使用C风格字符串作为函数的参数时,只需要将字符串的地址传过去就可以了,当遇到空字符或者指定的字符时就停止

10,当一个函数需要返回C风格字符串时,它应该返回一个指针,如下面这一段代码

#include <iostream>
char * test(char ch, int num)
{
char * str = new char[num + 1];
str[num] = '\0';
while (num--)
{
str[num] = ch;
}
return str;
}
void main()
{
using namespace std;
char ch;
cin >> ch;
int num;
cin >> num;
char * str = test(ch, num);
cout << str << endl;
delete []str;
str = test('n', 15);
cout << str;
delete []str;
}

需要注意的是,当你在一个函数中使用new分配了内存,并且返回了指向了该地址的指针,那么一定记得要用delete回收这一块内存

11,C++可以在函数中返回一个结构,与数组名不一样的是,数组名表示的是数组中首个元素的地址,而结构名就单单只是结构的名称,要想获得该结构的地址需要使用&运算符,在函数中使用结构时,一般有3种方法

  • 按值传递,将整个结构作为参数;如果结构很大,在复制结构时会增加内存要求,降低系统运行的速度
  • 传递结构的地址,然后用指针来访问结构的内容;很多程序员倾向于使用这种方法,在C中不能按值来传递结构;使用这种方法时,在调用函数时应该将结构的地址传递过去;在函数原型中需要将形参声明为相应结构体的指针;在函数中运用指针来访问相应的成员
  • 引用传递,引用传递使用的也是原始数据而不是数据的副本

按值传递使用的是结构的副本,传递结构的地址使用的是结构的原始数据,使用原始数据有损坏数据的风险,可以使用const关键字来防止误改数据

12,函数与string对象,与C风格字符串相比,在函数中使用string对象与使用结构更加相似,如一下代码所示。在读取输入时,如果使用cin则遇到回车符空格符会停止读入,使用getline可以读取一行的输入

#include <iostream>
#include <string>
using namespace std;
const int str_length = 3;
void show_str(const string str[], int num)
{
for (; num > 0; num--)
{
cout << str[num-1] << endl;
}
}
int main()
{
string str[str_length];
for (int i = 0; i < str_length; i++)
{
getline(cin, str[i]);
//cin >> str[i];
}
show_str(str, str_length);
}

13,函数与array对象,与处理结构时相同,可以将array对象的值传递给函数,也可以将array对象的地址传递过去;要注意的是模板array并非只能存储基本的数据类型,它也可以存储类对象(所以模板vector也可以?)

14,函数递归,函数自己调用自己叫做函数递归,如果没有到函数的递归出口,那么它会一直循环下去,main()函数不能递归。

#include <iostream>
void test(int n)
{
using namespace std;
cout << "Hello World!" << n << endl;
if (n > 0)
{
test(n - 1);
}
cout << "Nice Dude!" << n << endl;
}
int main()
{
test(4);
}

上述代码中,当n>0时,函数就会执行递归,位于递归前的代码将会按函数调用的顺序执行,位于递归后的代码将会按照与函数被调用的相反的顺序去执行,每一次递归调用都会创建自己的一套变量

15,函数指针,函数也有地址,通常该地址为存储其机器语言代码的内存的开始地址

  • 获取函数的地址,只需要使用函数名即可,后面不需要参数,要注意区分函数地址与函数的返回值
  • 声明函数指针,声明函数指针时,应该说明该函数的返回类型以及函数的参数,即函数声明函数指针时应该像声明函数原型时一样,可以理解成将函数名称替换成指针的名称
double test(int);//函数原型
double (*pf)(int);//函数指针
void test1(double (*pf)(int));//假如要将一个函数的地址作为参数传递给另一个函数,该函数的函数原型应为

 

  • 运用函数指针来调用函数
#include <iostream>
void show(int num)
{
	for (int i = 0; i < num; i++)
	{
		std::cout << "Hello World!" << std::endl;
	}
}
int main()
{
	int num;
	std::cin >> num;
	void (*fp) (int)=show;
	show(3);//调用函数
	(*fp)(2);//调用函数
	fp(1);//调用函数
}

上述代码中用函数指针fp指向了函数show,因此可以用3种不同的方法去调用show函数

16,下面代码是函数指针的一个示例,利用函数指针,将两个不同的函数内嵌到另一个函数内,这样的话不需要修改那另一个函数即可实现不同的操作。在使用函数指针时,可以使用C++11的关键字auto还有typedef来简化操作

#include <iostream>
int cal_minutes(int hours)
{
return hours * 60;
}
int cal_seconds(int hours)
{
return hours * 3600;
}
void show(int hours,int(*fp)(int))
{
using namespace std;
cout << (*fp)(hours) << endl;
cout << fp(hours) << endl;
}
int main()
{
using namespace std;
int hours;
cin >> hours;
show(hours, cal_minutes);
show(hours, cal_seconds);
}

练习题笔记:

1,当指针作为函数参数传递时,在函数内部重新申请了一个新指针,与传入指针指向相同地址。在函数内部的操作只能针对指针指向的值。以下代码中,main()函数中的p指针与test()函数内的p指针指向的地址是相同的,但是它们是两个不同的指针

#include <iostream>
using namespace std;
void test(int * p)
{
cout << p << endl;
cout << &p << endl;
}
int main()
{
int x;
int * p = &x;
cout << p << endl;
cout << &p << endl;
test(p);
}

2,在定义全局变量count时,会出现一个count全局变量不明确的问题,是因为在命名空间std中也有count,编译器不能确定是std中的count还是你自己定义的count

函数探幽

1,C++中的内联函数可以提高程序运行的速度,调用常规函数时,当程序遇到调用函数的指令,程序会储存当前执行的指令的地址,然后跳转到要调用的函数的地址,执行该函数,或许还会将函数的返回值存到寄存器中,然后在再返回到一开始保存的指令的地址处,所以在程序执行的过程中,会发生很多这样的跳转。使用内联函数,编译器会将函数调用换成相应的函数的代码,这样在调用内联函数时就不需要进行地址的跳转,可以节省时间;不过这种做法需要额外的内存,多一处内联函数,就多一份该函数的副本,所以我们应该有选择性地使用内联函数。当函数的执行时间很长时,函数调用的时间只占函数运行总时间的很少一部分,此时使用内联函数节省的时间就很有限;假如函数的执行时间很短,则使用内联函数可以节省调用该函数花费的时间,不过由于该时间很短,所以节省的时间并不会很多,除非该函数经常被调用。使用关键字inline来声明内联函数。inline是C++新增的特性,在C中使用的是宏,宏是内联函数的原始实现,宏的本质是文本的替换,而内联函数可以实现参数的传递,宏不可以进行参数的传递

2,引用变量的主要用途是用作函数的形参,使用引用变量作为形参是,函数使用的是原始数据而不是数据的副本。C和C++都使用&来表示变量的地址,C++还可以用它来表示引用,下面代码中,y是x的引用,他们指向相同的值和相同的内存单元,引用和指针有一定的相似之处。声明引用时一定要将其初始化,指针可以先声明再赋值,引用更接近const指针,一旦它跟一个变量关联起来,它就一直效忠于该变量。

int x;
int & y = x;

下面代码中,cube()函数不会修改x的值,而refcube()会修改x的值,由此可知引用传递使用的是原始数据,如果只是想使用引用变量的值而不改变它,应该使用const关键字,这样如果你在函数中修改了引用的变量的值,编译器将会提示错误

int cube(int x)
{
x *= x * x;
return x;
}
int refcube(int & x)
{
x *= x * x;
return x;
}
int main()
{
int x = 3;
cout << x << " " << cube(x) << endl;
cout << x << " " << refcube(x) << endl;
}

如果按值传递参数,那么我们可以采用多种形式的实参

int x = 3;
int result;
result = cube(x);
result = cube(x + 1);
result = cube(2);

如果采用参数传递,那么实参一定要符合引用参数的类型,引用传递的要求更为严格,使用引用传递实参不能为表达式;如果实参与引用参数的类型不匹配,那么C++将会生成临时变量,不过只有在引用参数使用了const的情况下才可以这么做

如果引用参数是const,那么会在以下两种情况下创建临时变量

  • 实参的类型正确,但不是左值
  • 实参的类型不正确,但可以被转换成正确的类型

左值参数指的是可以被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针;非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式

int refcube(const int & x)
{
return x * x*x;
}
int main()
{
int side = 3;
int *p = &side;
int &a = side;
long b = 3L;
int arr[3] = { 1,2,3 };
int c1 = refcube(side);//x是side
int c2 = refcube(*p);//x是*p是side
int c3 = refcube(a);//a是side
int c4 = refcube(b);//类型不正确,需要创建临时变量
int c5 = refcube(arr[2]);//x是arr[2]
int c6 = refcube(5);//类型正确,但没有名称,创建临时变量
int c7 = refcube(side + 2);//类型正确,但没有名称,创建临时变量
}

生成的临时匿名变量只在函数调用期间存在,在此之后,编译器可以将其随意删除。创建临时变量的行为会阻止函数想要修改引用传递过来的实参

我们应该尽量把引用参数声明为常量数据

  • 使用const可以避免无意中修改数据的编程错误
  • 使用const可以让该引用参数能够接受const和非const实参,否则只能接受非const数据
  • const引用使函数能够正确生成并使用临时变量

3,C++新增了右值引用,这种引用可以指向右值,使用&&来声明,使用&来声明的是左值引用

4,引用非常适合用于结构,而非基本内置的类型,因为结构中的成员可能会占用大量的内存,如果采用值传递的方式会浪费很多内存,一个函数在使用引用传递参数时,如果需要返回一个结构,其函数原型与采用值传递参数的函数不相同

student test(student stu1, const student stu2)
{
stu1.age += stu2.age;
return stu1;
}
student  & test1(student &stu1,const student stu2)
{
stu1.age += stu2.age;
return stu1;
}

5,返回引用与传统的返回机制不同,传统的返回机制与函数值传递参数类似,它先将返回值复制到一个临时位置,然后程序再在这个临时位置使用这个值;如果是返回引用,则程序将直接使用返回值,而不会先把它复制到一个临时位置,返回引用的效率会更高。常规函数的返回值是右值,正是因为这样的返回值是放在临时位置,所以它是右值

6,使用引用参数的两个主要原因

  • 程序员可以修改调用函数中的数据对象
  • 通过传递引用而不是整一个数据对象,可以提高程序运行的效率

当数据对象比较大的时候,第二个原因最为重要,同时这个也是使用指针参数的原因,引用实际上是基于指针的代码的另一个接口

下面是一些何时使用引用参数何时使用值传递,何时使用指针的建议

当只需要传递值而不用修改

  • 如果数据对象很小,比如内置数据类型或者小型结构,那么可以按值传递
  • 如果数据对象是数组,只能选择采用指针传递,同时应该将其声明为指向const的指针
  • 如果数据对象是比较大的结构,可以使用const指针或者const引用,这样可以提高程序运行的效率,节省复制结构的时间和空间
  • 如果数据对象是类对象,则使用引用传递

当需要修改值的时候

  • 如果是内置的数据类型,则使用指针,因为使用指针的话,在调用该函数时,参数是一个地址,跟采用引用传递相比,可以更加明显地看出该函数会修改实参的值
  • 如果数据对象是数组,则只能使用指针
  • 如果数据对象是结构,则可以使用引用或者指针
  • 如果数据对象是类对象,则使用引用

7,通过函数原型,可以设置函数的默认参数;带默认参数的函数在参数列表中必须从右往左添加默认值,也就是说假如要为某个参数添加默认值,要为它右边的所有参数添加默认值;实参会从左到右依次将值赋给形参,因此调用函数时不能跳过参数;对于带默认参数的函数,它只在函数原型中指定了默认值,函数定义与没有默认参数时相同

void test(int i, int j = 1);//valid
void test1(int i = 1, int j);//invalid
test(1);//valid
test(1, 2);//valid
test(, 2);//invalid
test();//invalid

8,函数重载指的是函数多态,它们是同一个意思;默认参数允许你用不同数量的参数去调用函数,而函数重载则允许有多个同名的函数,它们使用不同的参数列表;函数重载的关键是参数列表,也成为函数特征标,如果两个函数的参数数目和类型相同,则它们的函数特征标相同,C++允许定义名称相同的函数,前提是它们的函数特征标不同,在调用函数时,编译器会根据调用时传递的参数调用有相应特征标的原型

9,下列代码中调用print()时会出错,因为当出现调用函数的实参与函数原型形参的类型不匹配时,C++将会尝试强制类型转换,下列代码中3个函数原型都是将数字作为参数的,因此C++将不知道需要采用哪个原型,转换成哪种类型,所以会出错;假如3个函数原型中只有1个是以数字作为形参的话,那么C++将会采用这种强制类型转换

#include <iostream>
using namespace std;
void print(int i)
{
	cout << i << endl;
}
void print(double i)
{
	cout << i << endl;
}
void print(long i)
{
	cout << i << endl;
}
int main()
{
	unsigned int i = 100;
	print(i);//invalid
}

10,一些看起来不相同的特征标是不能共存的,比如下面的代码,调用函数时,编译器不能知道需要使用哪一个函数原型;因此编译器会将类型本身和类型引用视为同一个特征标

void print(int i);
void print(int & i);
print(i);

11,函数重载的关键是特征标,如下,不能仅仅以不同的返回类型来重载函数,返回类型可以不相同,但同时特征标也不能相同

int test(int n, float m);//valid
double test(float n, float m);//valid

int test(int n, float m);//invalid
double test(int n, float m);//invalid

12,重载引用参数,以下三种引用的重载,它们可以匹配的参数类型也是不相同的,const左值引用可以同时匹配所有引用类型的参数,当重载使用这三种参数的函数,将会自动选用最为匹配的版本

void test(int & i);//匹配可以修改的左值参数
void test(const int & i);//匹配可以修改的左值参数,不可修改的左值参数以及右值参数
void test(int && i);//匹配右值参数
double x=1.5;
const double y=2.5;
test(x);//调用test(int & i)
test(y);//调用test(const int & i)
test(x+y);//调用test(int && i),假如没有定义test(int && i),则会调用test(const int & i)

13,不能滥用函数重载,如果能够利用默认参数就能解决的问题应该使用默认参数,仅当函数需要对不同类型的参数执行相似的任务时,就使用函数重载;使用一个带默认参数的函数会简单些,程序员只需要修改一个函数,程序也只需要为一个函数申请内存

14,函数模板,它可以被称为通用的函数描述,在函数模板中,将参数的类型作为一个参数来进行传递,使用函数模板可以节省大量重复的代码和时间;函数模板并不能缩短可执行的程序,最终的程序中不会包含任何模板的内容,而是实际的函数,函数模板可以使生成多个函数时不需要重复工作,模板使这个过程更简单,更可靠

#include <iostream>
using namespace std;
template <typename anytype>//typename可以用class替代
void myswap(anytype & a, anytype & b)
{
	anytype temp;
	temp = a;
	a = b;
	b = temp;
}
int main()
{
	int x = 1;
	int y = 2;
	char ch1 = 'a';
	char ch2 = 'b';
	cout << x << endl << y << endl;
	myswap(x, y);
	cout << x << endl << y << endl;
	cout << ch1 << endl << ch2 << endl;
	myswap(ch1, ch2);
	cout << ch1 << endl << ch2 << endl;
}

并非所有模板参数都必须是模板参数类型,比如

void test(Mytype & x, Mytype & y, int i);

函数模板也可以进行重载,比如

template <typename Mytype>
void test(Mytype & x, Mytype & y, int i);
template <typename Mytype>
void test(Mytype & x, Mytype & y);

模板也有其局限性,有时候编写好的模板可能会无法处理某些类型

#include <iostream>
using namespace std;
template <typename Mytype>
void test(Mytype a, Mytype b)
{
	cout << a << endl << b << endl;
	cout << a + b << endl;
}
int main()
{
	int a[] = { 1,2,3 };
	int b[] = { 1,2,3 };
	cout << a << endl << b << endl;
	test(a, b);
}

上面这一段代码会出错,因为a和b是两个数组,利用模板将该函数实现之后,会对a,b执行相加操作,而两个指针是无法相加的。遇到这一种情况时,有两种解决办法,一种是重载运算符;另一种是为特定的类型提供特定的模板定义,这种操作称为显式具体化

具体化的方法有三种,在C++98标准中

  • 对于一个特定名称的函数,它可以有非模板函数,模板函数,显式具体化的模板函数以及它们的重载函数
  • 具体化的模板优先于常规模板,非模板函数优先于具体化模板和常规模板
  • 显式具体化的原型和定义都以template <>开头,并且指出该具体化的模板的类型

实例化与具体化是两个不同的概念,实例化可以分为显式实例化与隐式实例化

  • 当编译器使用模板为特定类型生成函数定义时,我们将该函数定义成为模板实例,在如下代码中,当我们调用test()时,编译器会根据模板生成一个test()的模板示例,同时又因为传递的实参为int类型,因此最终生成了一个类型为int的函数定义,这个过程是隐式实例化
#include <iostream>
using namespace std;
template <typename Mytype>
void test(Mytype & a, Mytype & b)
{
	cout << a << endl << b << endl;
	cout << a + b << endl;
}
int main()
{
	int a = 2;
	int b = 2;
	cout << a << endl << b << endl;
	test(a, b);
}
  • C++允许进行显式实例化,即根据模板来创建特定的类型,其语法是<>来声明要创建的类型,同时在声明之前加上template关键字,如下代码中将模板实例化成参数为double类型的,两个实例化的语句的作用是一样的,因为参数中已经说明参数为double类型,同时在编译时还发现main()函数中不能进行显式实例化
#include <iostream>
using namespace std;
template <typename T>
void test(T & a, T & b)
{
	cout << a << endl << b << endl;
	cout << a + b << endl;
}
template void test(double & a, double & b);
template void test<double>(double & a, double & b);
  • 与显式实例化不同,显式具体化需要在声明前面加上template <>,而且显式具体化一定要有它自己的函数定义,这个声明的意思是不要使用模板去实例化这种类型的函数,而应该使用为这种类型显示具体化的函数定义

15,对于函数重载,模板函数以及模板函数重载,C++有一个良好的策略来选择适用的函数,这个过程称为重载解析

  • 第一步是先找到与调用参数名称相同的函数以及模板函数,创建候选函数列表
  • 第二步是使用候选函数创建可行函数列表,可行函数列表中的函数的形参数量与调用时实参的数量完全一致,这个过程中还会包含隐式类型转换,在确定在可行函数列表中哪个函数是最佳的选择,优先度如下:1,实参与形参完全匹配,这种情况下常规函数优于模板函数。2,实参到形参需要执行提升转换(例如short,char到int,float到double)3,实参到形参需要执行标准转换(int到char,int到double等)4,用户定义的转换
  • 第三步确定是否有最佳的可行函数,如果没有则调用出错

当有多个匹配的原型时,编译器会无法完成重载解析过程,但是有些情况下,出现多个匹配的原型仍然可以完成重载解析。例如1,指向非const数据的指针和引用与指向const数据的指针和引用,如果实参是const数据,则带有const的原型更为匹配,如果实参不是const数据,则不带有const的原型更为匹配,这种const和非const之间的区别只适用于指针传递和引用传递。2,一个是非模板函数而另外一个不是,则应选择非模板函数。3,两个都是模板函数,则应选用更具体的函数,显示实例化的函数优于隐式实例化的函数。更具体并不意味着是显示实例化或者显示具体化,而是在使用时执行的转换更少,例如在下列代码中test()将会采用template <typename T> void test(T * p)这一个原型,因为采用它执行的转换更少,即test<int>(int *);如果采用template <typename T> void test(T p),则它的转换为test<int *>(int *),在前者中模板里已经将参数具体化为指针,所以相比起来它会更具体

#include <iostream>
using namespace std;
template <typename T> void test(T * p);
template <typename T> void test(T p);
int main()
{
	int a = 100;
	test(&a);
}
test();//有常规函数,选择常规函数;否则选择函数模板
test<>();//一定是使用函数模板,而不会使用常规函数,从隐式实例化,显示实例化,显示具体化中选择
test<int>();//一定使用函数模板,不会使用常规函数,显示实例化为int类型

16,关键字decltype(C++11新增)

在下面这种情况中,a+b的类型是不能确定的,这将会根据a和b的类型来变化,如果想要以固定的类型来输出该结果,则需要使用decltype关键字

template <typename T1,typename T2> void test1(T1 & a, T2 & b)
{
	cout <<(a + b) << endl;
}

修改后的代码如下,a+b的结果会以int类型输出

template <typename T1,typename T2> void test1(T1 & a, T2 & b)
{
	cout << decltype (int) (a + b) << endl;
}

decltype的格式为

decltype <expression> var;

 

  • 如果expression是一个没有用括号括起来的标识符,则var的类型与它的类型一样,包括const等限定符
  • 如果expression是一个函数调用,则var的类型与该函数返回值的类型相同,实际上此时并不会调用该函数,而是通过查看函数原型得到该函数返回值的类型
  • 第三步书本讲得并不明白,大体含义如下面代码所示
double xx = 4.4;
decltype ((xx)) r2 = xx;//r2的类型是double &
decltype (xx) w = xx;//w的类型是double
  • 如果上述条件都不满足,则var的类型和expression一样

17,另一种函数声明语法,有一种情况在声明函数时是decltype也无法解决的

template <typename T1,typename T2>
? type ? test2(T1 & a, T2 & b)
{
	return a + b;
}

在这种情况下,不能采用decltype(a+b)作为返回类型,但是此时并未声明a,b它们并不在作用域内,如若要使用decltype必须在声明参数之后,为此可以使用一种新的声明和定义函数的语法

double test1(double & a, double & b)
{
	return a + b;
}
auto test2(double & a, double & b)->double
{
	return a + b;
}

这两种定义函数的方法是等价的,新增的这种方法叫后置返回类型,auto是一个占位符,这是C++11中给auto新增的一个角色,利用后置返回类型和decltype可以解决遇到声明返回类型时遇到的问题,此时decltype在参数声明之后,所以我们可以使用它们

练习:

1,对数组使用引用传递时要结合模板,对数组使用引用时不再需要将数组大小作为参数传递,在定义模板时设置一个变量来表示数组的大小,代码如下

/*
void fill(double arr[], int size)
{
	for (int i = 0; i < seasons; i++)
	{
		cout << "Enter " << str[i] << " expenses: ";
		cin >> arr[i];
	}
}
*/
template <typename T,int size> void fill(T (&arr)[size])
{
	for (int i = 0; i < seasons; i++)
	{
		cout << "Enter " << str[i] << " expenses: ";
		cin >> arr[i];
	}
}
/*
void show(double arr[], int size)
{
	double total = 0;
	cout << "\nEXPENSES\n";
	for (int i = 0; i < seasons; i++)
	{
		cout << str[i] << ": $" << arr[i] << endl;
		total += arr[i];
	}
	cout << "Total Expenses: $" << total << endl;
}
*/
template <typename T, int size> void show(T (&arr)[size])
{
	double total = 0;
	cout << "\nEXPENSES\n";
	for (int i = 0; i < seasons; i++)
	{
		cout << str[i] << ": $" << arr[i] << endl;
		total += arr[i];
	}
	cout << "Total Expenses: $" << total << endl;
}

内存模型和名称空间

1,作用域描述了名称在文件的多大范围内可见,例如在函数中定义的变量只能在该函数内使用;链接性描述了名称如何在不同单元之间共享,链接性为外部的名称可以在文件间共享,链接性为内部的名称只能由一个文件中的函数共享

2,局部变量的作用域为定义它的代码块,全局变量在定义位置直到文件末尾都可以使用,文件作用域等同于全局的意思,静态变量的作用域是全局还是局部取决于它是如何被定义的。在函数原型作用域中声明的变量只可以在函数定义中使用,在类中声明的成员的作用域为整个类,在名称空间中声明的变量的作用域是整个名称空间,C++函数的作用域是整个类或整个名称空间,但不能是局部的,因为不能在代码块中定义函数,如果函数的作用域是局部的,那么它只对自己可见,不能被其他函数调用,这样的函数无法运行

3,C++中管理内存的方式有四种,分别是自动存储,静态存储,动态存储和C++11中新增的线程存储

自动存储

  • 在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性,当程序开始执行这些变量所属的代码块时,将为变量分配内存,当函数结束时,这些变量都将消失
  • 可以使用任何在声明时其值为已知的表达式来初始化自动变量
  • C++用栈来实现自动变量,栈的特点是后进先出,程序使用两个指针来跟踪栈,一个指向栈底,即栈的开始位置,一个指向栈顶,即下一个可用内存单元。当函数结束时,栈顶指针被重置为函数被调用前的值

静态存储

  • C++为静态存储持续性变量提供了3种链接性,无链接性(只能在当前函数或代码块中访问),外部链接性(可在其他文件中访问),内部链接性(只能在当前文件中访问),由于静态变量的数目在程序运行期间是不变的,因此程序不需要用像栈这样特殊的结构去管理它们,编译器会分配固定的内存块来存储所有的静态变量
  • 如果没有显示地初始化静态变量,编译器会将它设置为0,数组和结构中的每个成员也都会被设置为0
  • 要想创建链接性为外部的静态变量,必须要在代码块外声明它;要想创建链接性为内部的静态变量,必须在代码块外声明它,并且使用static限定符;要想创建无链接性的静态变量,必须在代码块内声明它,并使用static限定符,所有静态变量在整个程序运行的过程中都会存在,z是无链接性的静态变量,作用域是局部,只可以在函数test()中使用它,这个跟自动变量z1一样,不过在函数没有被执行之前,z也会留在内存中。x,y的作用域是全局,从它们被声明的位置到文件末尾都可以使用,x的链接性为外部,在其他文件中也可以使用,y的链接性为内部,只可以在本文件中使用
#include <iostream>
using namespace std;
int x = 10;
static int y = 100;
void test()
{
	static int z = 1000;
	int z1 = 1000;
}
int main()
{

}
  • 静态变量的初始化有三种,零初始化,常量表达式初始化,动态初始化,其中零初始化和常量表达式初始化被称为静态初始化,静态初始化时在编译时就初始化变量,动态初始化是在编译后初始化变量。在初始化的过程中,所有的静态变量会先被零初始化,然后,如果使用了常量表达式初始化了变量,且编译器根据文件内容就可以计算表达式的话,编译器将会执行常量表达式初始化,如果没有足够的信息,变量将被动态初始化
#include <iostream>
using namespace std;
int x;//零初始化
int y = 13;//常量表达式初始化
int z = 13 * 13;//常量表达式初始化
int x1 = 13 * test(2);//动态初始化,因为这里要通过计算test()的返回值来进行计算
  • 静态变量的外部链接性,一方面,在每个使用外部变量的文件中都必须声明它,另一方面C++中有单定义规则,即变量只能有一次定义,为了满足这种需求,C++提供了两种变量声明,一种是定义声明或简称为定义,这种方式会为变量分配存储空间;另一种是引用声明,简称为声明,它不给变量分配存储空间,因为它引用已有的变量。引用声明使用关键字extern,且不进行初始化;否则为定义声明,导致分配存储空间。在多个文件中使用外部变量时,只需要在一个文件中定义它即可,但在使用该变量的其他文件中都必须使用关键字extern。单定义规则并不意味着不能有多个名称相同的变量,例如可以在不同的函数中声明的同名的自动变量,局部变量可能会隐藏同名的全局变量
  • 当在函数中定义一个与全局变量同名的局部变量时,局部变量会隐藏全局变量,当局部变量将全局变量隐藏起来时,我们可以使用作用域解析运算符(::)来访问变量的全局版本
//external.cpp
#include <iostream>
using namespace std;
double warming = 0.3;
void update(double dt);
void local();
int main()
{
	cout << "Global warming is " << warming << " degrees.\n";
	update(0.1);
	cout << "Global warming is " << warming << " degrees.\n";
	local();
	cout << "Global warming is " << warming << " degrees.\n";
	return 0;
}
//support.cpp
#include <iostream>
extern double warming;
using std::cout;
void update(double dt)
{
	extern double warming;
	warming += dt;
	cout << "Updating global warming to " << warming;
	cout << " degrees.\n";
}
void local()
{
	double warming = 0.8;
	cout << "Local warming = " << warming << " degrees.\n";
	cout << "But global warming = " << ::warming;
	cout << " degrees.\n";
}
  • 全局变量的优点在于所有的函数都可以直接访问这些变量,而不需要进行参数传递,但这样会导致程序不可靠,这样会导致程序对数据进行不必要的访问,不利于保持数据的完整性,所以通常情况下我们应该使用局部变量,应该在该函数需要使用这些变量时才传递给它
  • 静态变量的内部链接性,如果要在一个文件中使用的变量的名称与已经在其他文件中定义了的常规外部变量相同时,需要使用关键字static来表明这个变量的内部链接性。如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同时,则在该文件中,静态变量将隐藏常规外部变量。使用外部变量在多文件程序的不同部分之间共享数据,可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据(名称空间提供了另外一种共享数据的方法)
  • 无链接性的局部变量,定义无链接的局部变量时是在代码块内定义并且带上static限定符,它只在它被定义的代码块中可用,但这个代码块不处于活动状态时,这个无链接的局部变量依旧存在,以下代码的输出结果为10 11
#include <iostream>
using namespace std;
void test()
{
	static int a = 10;
	cout << a << " ";
	a++;
}
void main()
{
	test();
	test();
}

动态存储

  • 使用C++运算符new分配的内存被称为动态内存,动态内存由new和delete控制,而不是由作用域和链接性规则控制,因此可以在一个函数中分配动态内存,而在另外一个函数中将其释放。其分配和释放的顺序要取决于new和delete在何时以何种方式被使用,通常编译器会使用三块独立的内存来存放自动变量,动态变量以及静态变量(可能会再细分)虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量,例如下面的语句,由new分配的80字节的内存将会一直保存,直到使用delete运算符来将其释放,当包含该声明的语句块被执行完之后,p_fees指针将被销毁,如果希望在语句块之外使用分配的这些内存,那么应该将这块内存的地址传递出去;另外如果将p_fees的链接性声明为外部的话,那么在该文件中位于该声明之后的所有函数都可以使用它,如果希望在另外一个文件之中使用,则需要在另外一个文件中使用声明extern float * p_fees
float * p_fees = new float [20];
  • new运算符可以分为常规new运算符和定位new运算符,常规new运算符负责在堆(heap)中找到一个符合要求的内存块,定位new运算符能够让你指定要使用的位置,要是用定位new特性首先要包含头文件new。在使用定位运算符时,要注意分配的内存是否为静态内存,如果是静态内存则不能使用delete来释放定位运算符new分配的内存,因为delete只能释放由常规new运算符分配的内存,用delete来释放静态内存将会出错。定位new运算符的工作原理是返回传递给它的地址,并将其强制转换为void *类型,以便能够赋给任何指针类型
char buffer[500];//一块内存空间
int * p1 = new (buffer) int [20];//定位new运算符格式,int数组使用这个buffer的空间

4,C++中有些说明符和限定符被称为存储说明符或者cv-限定符,这些关键字提供了其他有关存储的信息,下列是存储说明符

  • auto(在C++11中不再是说明符,之前用于显示声明自动变量,现在用于自动类型推断)
  • register(用于声明中指示寄存器存储)
  • static(被用在作用域为整个文件的声明中时,表示其内部链接性;被用在作用域为局部的声明时,表明其无链接性)
  • extern(表示引用声明,在使用其他文件定义的外部变量时要使用这个关键字)
  • thread_local(C++11新增的,表示该变量的持续性与其所属线程的持续性相同)
  • mutable(它用来指出即使某个结构或者类为const,其某个成员也可以被修改)

cv限定符包括了const,volatile。volatile表明,即便程序没有对内存单元进行修改,其值也可能发生变化,即该变量类型可以被某些编译器未知的因素修改,比如:操作系统,硬件或者其他线程等,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,优化代表着可以提供对特殊地址的稳定访问,从而每次遇到这个用volatile声明的变量时,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据

volatile int i = 10;
int a = i;
/*...其他代码,并未明确告诉编译器对i进行过操作*/
int b = i;

如果不使用volatile的话,由于编译器进行了优化,编译器发现两次从i读取数据的代码之间没有对i进行过操作,它会自动把上次读的数据放在b中,这样有可能会出错,i的值仍然为赋给a的值10;使用了volatile之后,每次都会从该变量所在的内存读取数据,从而保证对特殊地址的稳定访问

最常用的是const,它表明内存被初始化之后,程序便不能再对它进行修改。默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的,在C++看来,全局const定义就像使用了static说明符一样,这一点const比较特殊。这样的话当将一组常量放在头文件中时,并在程序的多个文件中使用该头文件,那么在预处理器将头文件内容包含到每个源文件中后,所有源文件都会包含这些常量的定义,假如在C++中const没有内部链接性这个特点,那么在将其包含到多个源文件中时就是重复定义,将会导致错误,内部链接性还意味着每个文件都有一组自己的常量,而不是所有文件共享一组常量,每个定义都是所属文件私有的。如果出于某种原因,程序员希望const常量的链接性为外部的,则可以使用extern来覆盖它原来默认的内部链接性,这跟常规外部变量不一样,在定义常规外部变量的时候,不必使用extern关键字,只有在其他文件使用该变量时使用extern关键字,然而鉴于const常量的值不能被修改,所以只能有一个文件对其进行初始化。在函数或者代码块中声明const的时候,其作用域为代码块

5,函数也具有链接性,所有函数的存储持续性都自动为静态的,即在整个程序执行的阶段都一直存在,默认情况下,函数的链接性为外部的,可以在文件之间共享,可以使用extern关键字来指出一个函数是在另外一个文件中定义的,也可以使用static关键字使函数的链接性为内部,必须在函数原型和函数定义中都使用这个关键字。C++在查找函数定义时遵循以下的顺序,如果该文件中的函数原型指出该函数是静态的,则编译器将只在该文件中查找函数定义,否则编译器将会在所有的程序文件中查找,如果找到两个定义,编译器将发出错误的消息,因为每个外部函数只能有一个定义,如果在程序文件中没有找到,则编译器会在库中搜索,这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数

6,C++名称空间是一种创建命名的新的声明区域,它可以提供一个声明名称的区域,一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西,例如下面的代码使用新的关键字namespace创建了两个新的名称空间

namespace Jack
{
	double pail;
	void fetch();
	int pal;
	struct Well{};
}
namespace Jill
{
	double bucket(double n) {};
	double fetch;
	int pal;
	struct Hill{};
}

名称空间可以是全局的,也可以位于另外一个名称空间中,但不能位于代码块中,因此在默认情况下,在名称空间中声明的名称的链接性是外部的(除非它引用了常量,引用了常量之后的链接性是内部的)名称空间是开放的,可以把名称加入到已有的名称空间之中,未被装饰的名称称为未限定的名称,包含了名称空间的名称称为限定的名称

访问名称空间中的名称有3种方法

  • 通过作用域解析运算符(:),例如下面代码通过:来访问Jill中的pal和Jack中的pail
Jill::pal;
Jack::pail;
  • 通过using声明,using声明使特定的标识符可用
using std::cout;
using std::endl;
  • 通过using编译指令,using编译指令使整个名称空间可用
using namespace std;

using编译指令和using声明它们会增加名称冲突的可能性,也就是说,运用作用域解析运算符是不会产生二义性的。使用using编译指令导入一个命名空间与使用多个using声明,using编译指令更像是大量使用了作用域解析运算符,使用using声明就好像声明了相应的名称一样,假设名称空间和声明区域定义了相同的名称,如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突;如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将会隐藏名称空间版本。一般来说,使用using声明要比使用using编译指令安全,这是因为它只导入指定的名称,如果该名称与局部名称发生冲突,那么编译器会发出指示;如果使用using编译指令导入所有的名称,有可能会导入不需要的名称,如果导入的名称与局部名称发生冲突的话,局部版本将会覆盖名称空间版本,而编译器不会发出警告。所以应该尽量使用using声明和作用域解析运算符或者嵌套式名称空间来访问名称空间

名称空间的嵌套,代码中flame指的是elements::fire::flame,也可以使用using namespace elements::fire使fire中的名称可用

#include <iostream>
namespace elements
{
	namespace fire
	{
		int flame;
	}
	float water;
}
int main()
{
	using namespace elements::fire;
	elements::fire::flame;
}

也可以在名称空间之中使用using编译指令和using声明,假设要访问element::water,现在它位于名称空间myth中,在这里它被称作water,所以我们可以这样访问它myth::water,当然它也位于名称空间elements中,因此也可以用elements::water来访问,如果没有与之冲突的局部变量,则也可以通过water直接访问

namespace myth
{
	using elements::water;
	using namespace elements;
	using std::cout;
	using std::cin;
}

未命名的名称空间,我们可以通过省略名称空间的名称来创建未命名的名称空间,这就像后面跟着using编译指令一样,这种名称空间没有名称,因此不能显式地使用using编译指令或者using声明来使它在其他位置都可用,具体地说是不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。这就提供了链接性为内部的静态变量的替代品

static int counts;
namespace
{
	int counts;
}

5,C++在开发程序时大多使用多个文件,一种有效的组织策略是,使用头文件来定义用户类型,为操纵用户类型的函数提供函数原型,并将函数定义放到一个独立的源代码文件中,最后将main()和其他使用这些函数的函数放在第三个文件中

练习:

1,由cin.get()和cin.getline()的函数原型可知,这两个函数接受的参数是一个字符数组的首地址和数组长度,所以不能用来读取输入到string类中,如果想要用这两个函数将输入读取到string中,则需要用以下方式;在这一章的练习题1和练习题2中总结得出,如果需要利用判断输入的字符串是否为空字符串来结束输入的话,用getline()来读取输入,getline()会丢弃换行符,从而能够顺利地判断一个字符串是否为空字符串,如果采用cin方法输入,换行符的存在会给判断带来麻烦

string input;
getline(cin, input);

类与对象

1,面向对象编程(OOP)有几个最重要的特性

  • 抽象
  • 封装与数据隐藏
  • 多态
  • 继承
  • 代码的可重用性

2,接口是一个共享框架,供两个系统交互时使用;对于类我们说公共接口,在这里,公众是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成,接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。例如要计算string对象中包含多少个字符,无需打开该对象,只需要使用它提供的size()或者length()方法,类设计禁止公共用户直接访问类,但我们可以使用size()方法。要使用类,必须了解其公共接口,要编写类,必须创建其公共接口

3,其定义位于类声明中的函数都将自动成为内联函数,类声明常将短小的成员函数作为内联函数;如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数。为此,只需在类实现部分中定义函数时使用inline限定符即可,内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义,确保内联定义对多文件程序中的所有文件都是可用的,最简便的方法是将内联定义放在定义类的头文件中;在类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写成内联函数

4,对于一个类,它的所有对象都有自己的存储空间,用于存储其内部变量和类成员,但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本

5,类不能像结构体那样来初始化它的数据成员,因为类对象中的数据成员是私有的,只可以通过公有的成员函数来访问;假如将类对象中的数据成员的属性设置为公有的,这样就不符合类隐藏数据的初衷;因此初始化类对象需要使用构造函数,构造函数可以在创建新对象时,自动对它进行初始化。构造函数没有返回值,不需要声明其类型,它的原型位于类声明的公有部分。

使用构造函数有两种方法,一种是显式使用构造函数,一种是隐式使用构造函数,假设Stock是一个类,两种调用方式是等价的,隐式调用更为简洁

  • 显式使用
Stock obj = Stock(1,2,3);
  • 隐式使用
Stock obj(1,2,3);

无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的,因此构造函数被用来创建对象,而不能通过对象来调用

默认构造函数是提供显式初始值时,用来创建对象的构造函数,它适用于这种声明的构造函数

Stock obj;

当程序员没提供任何构造函数时,C++将自动提供默认构造函数,它是默认构造函数的的隐式版本,不做任何工作。对于Stock来说,默认构造函数可能如下Stock::Stock() {},因此上述语句将创建obj对象,但不初始化其成员。当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数,如果为类定义了构造函数之后,那么必须为它提供默认构造函数,如果提供了非默认的构造函数,但没有提供默认构造函数,则会出错,如果想创建一个对象却不显式地初始化,则必须定义一个不接受任何参数的默认构造函数

定义默认构造函数的方式有两种

  • 一种是给已有构造函数的所有参数提供默认值
Stock(int i=1,int i=1,int i=1);
  • 一种是通过函数重载来定义另一个构造函数,一个没有参数的构造函数
Stock();

由于只能有一个默认构造函数,因此不要同时采用这两种方式,应初始化所有的对象,以确保所有成员一开始就有已知的合理值

构造函数不仅仅可用于初始化新对象,例如

obj = Stock(1,2,3);

假设obj是一个已经存在的Stock对象,这条语句不是对obj进行初始化,而是将新值赋给它。这是通过让构造程序创建一个新的、临时的对象,然后将其内容复制给obj来实现的,随后程序调用析构函数来删除这个临时对象

6,析构函数,和构造函数一样,也可以没有返回值和声明类型,跟构造函数不同的是,析构函数没有参数

~Stock();//析构函数的原型必须是这样的

7,const成员函数,若用const将一个对象声明为const常量,那么我们需要使用一种新的语法来确保函数不会修改该调用对象,解决方法是使用const成员函数

void test () const;

使用类

1,运算符重载,运算符重载的声明,重载之后的运算符也是由对象来调用的,有两种调用形式

//假设有一个time类
time operator+(const time & t1) const;
sum=t1+t2;//左边的对象是调用operator+的对象,右边的对象是作为参数被传递的对象
sum=t1.operator+(t2);

多数C++运算符都可以用这样的方式重载,重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型,下面是C++对用户定义的运算符重载的限制

  • 重载后的运算符必须至少有一个操作数是用户定义的类型
  • 使用运算符时不能违反运算符原来的句法规则,例如不能将求模运算符重载成使用一个操作数,同样不可以修改运算符的优先级
  • 不可以创建新的运算符
  • 不能重载下面的运算符
sizeofsizeof运算符
.成员运算符
.*成员指针运算符
::作用域解析运算符
?:条件运算符
typeid一个RTT1运算符
const_cast强制类型转换运算符
dynamic_cast强制类型转换运算符
reinterpret_cast强制类型转换运算符
static_cast强制类型转换运算符

 

 

 

 

 

 

 

 

 

 

大多数运算符都可以通过成员函数和非成员函数来进行重载,但这几个运算符只能通过成员函数进行重载

=赋值运算符
()函数调用运算符
[]下标运算符
->通过指针访问类成员的运算符

2,C++控制对类对象私有部分的访问,通常公有类方法是唯一的访问途径,但是C++还提供了另外一种形似的访问权限,友元,友元有三种,友元类,友元函数和友元成员函数。当重载二元运算符(带两个参数的运算符)时,假如两个操作数都为类的对象,这时不会出现问题,可以将两个操作数即对象跟运算符的相对位置调换,一般来说调用的是左边操作数的方法,右边的操作数作为参数进行传递;但是当两个操作数为不同类型时,它们与运算符的相对位置就不能随意更换,例如一个操作数为对象,一个操作数为double类型时,只有对象才能调用类中的方法,因此左边的操作数必须为对象。解决这种方法有两种方法,一种是在使用这个重载的运算符时,按照左边操作数是对象,右边操作数是其他类型的格式来进行编写,但是这种方法很麻烦;另一种方法是将重载该运算符的方法声明为非成员函数,将其声明为非成员函数后,可以按我们所需要的顺序获取操作数,常规非成员函数不能直接访问类的私有数据,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数

创建友元时,第一步,要先将其原型放到类声明中,并且在原型声明前加上关键字friend,虽然友元函数是在类声明中声明的,但它不是成员函数,不能使用成员运算符来调用它,虽然友元函数不是成员函数,但它与函数的访问权限相同;第二是编写函数的定义,因为友元函数不是成员函数,因此不需要使用::限定符,在定义中也不需要使用关键字friend。总之如果要为类重载运算符,并将非类的项作为第一个操作数,则可以用友元函数来反转操作数的顺序

3,重载运算符:作为成员函数还是非成员函数

一般来说,非成员函数应该是友元函数;例如重载一个二元操作符,对于成员函数版本来说,一个操作数通过this指针隐式地传递,另一个操作数作为函数参数显式传递,对于友元版本来说,两个操作数都作为参数来传递

4,类的自动转换和强制类型转换

在C++中接受一个参数的构造函数可以将与该参数类型相同的值转换为类,只有接受一个参数的构造函数才能作为转换函数,假如一个构造函数有两个参数,则其不能作为转换函数,但是假如除第一个参数外的其他参数有默认值,那么也可以将这个构造函数作为转换函数。

#include <iostream>
using namespace std;
class student
{
	int age;
public:
	student(int);
	student();
	void showstudent();
};
void student::showstudent()
{
	cout << age << endl;
}
student::student(int age_)
{
	age = age_;
}
int main()
{
	student stu1 = 10;
	stu1.showstudent();
}

上述代码可以将int的值自动转换为student类,这个过程中构造函数会创建一个临时的student对象,然后再采用逐成员赋值的方式将临时对象的内容复制到stu1中,这个过程称为隐式转换,它是自动进行的,而不需要显式强制类型转换。可以使用explicit关键字来关闭这种特性,使用了explicit关键字后,则这个带一个参数的构造函数将只用于显式强制类型转换

#include <iostream>
using namespace std;
class student
{
	int age;
public:
	explicit student(int);
	student();
	void showstudent();
};
void student::showstudent()
{
	cout << age << endl;
}
student::student(int age_)
{
	age = age_;
}
int main()
{
	student stu1 = student(10);
	student stu1(10);
	stu1.showstudent();
}

当不使用explicit关键字时,它除了可以用于显式强制类型转换,否则还可以用于下面的隐式转换

  • 将对象初始化为某种类型的值时
  • 将某种类型的值赋给对象时
  • 将某种类型的值传递给接受对象作为参数的函数时
  • 返回值被声明为对象的函数试图返回某种类型的值时
  • 在上述任意一种情况下,使用可以转化为上述的某种类型的内置类型时
#include <iostream>
using namespace std;
class student
{
	int age;
public:
	student(int);
	student();
	void showstudent();
};
void student::showstudent()
{
	cout << age << endl;
}
student::student(int age_)
{
	age = age_;
}
int main()
{
	student stu1 = 10.5;
	stu1.showstudent();
}

最后一点意味着可以用该构造函数来转换其他数值类型,仅当转换没有二义性时才能进行这种二步转换,下面的代码就出现了二义性

#include <iostream>
using namespace std;
class student
{
	int age;
public:
	student(int);
	student();
	void showstudent();
};
void student::showstudent()
{
	cout << age << endl;
}
student::student(int age_)
{
	age = age_;
}
int main()
{
	student stu1 = 10.5;
	stu1.showstudent();
}

我们还可以做与上述转换相反的转换,即将对象转换为某种类型的值,在C++中可以这样做,需要使用特殊的C++运算符函数,转换函数,转换函数是类成员,没有返回类型,没有参数

#include <iostream>
using namespace std;
class student
{
	int age;
public:
	student(int);
	student();
	void showstudent();
	operator int();
};
void student::showstudent()
{
	cout << age << endl;
}
student::student(int age_)
{
	age = age_;
}
student::operator int()
{
	return int(age);
}
int main()
{
	student stu1 = 10;
	stu1.showstudent();
	int x = stu1;
	cout << x << endl;
}

和转换构造函数一样,转换函数也提供自动,隐式的转换,在用户不希望转换时,它也有可能执行转换。因此可以使用explicit,explicit关键字同样可以用于转换函数,将转换函数声明为显式的;另一种方法是用一个功能相同的非转换函数替换这个转换函数,应该谨慎地使用隐式转换函数,通常最好选择仅在被显式调用时才会执行的函数

练习:

1,在一个类中定义了一个枚举,在类外使用该枚举时需要使用类名加上作用域解析运算符

2,在重载输入输出运算符时,必须使用返回引用和友元的方式来进行重载,否则编译器会报错

要使用返回引用的原因。当函数返回引用类型时,没有复制返回值,返回的是对象本身,参数传递的过程中不会发生创建临时变量和拷贝,引用的方式节省了资源,而且比较高效。对于返回引用,要求必须在函数的参数之中,包含有以引用方式或指针方式存在的,需要被返回的参数。如果两个参数都不是引用,则两个参数实际上都是存放在函数栈里的局部变量,也可以说是自动变量,返回它们的引用会出错,因为函数运行结束时,栈地址被回收了,这些地址上的数据已经不是原来那个值了。重载输入输出运算符的时候,其实就是在返回ostream和istream的引用,使用引用返回是因为如果不使用引用返回,那就只能使用值返回,值返回需要复制返回的对象,但是无法复制一个ostream对象,所以不能使用值返回,只能使用引用返回

需要使用友元的原因。因为<<和>>运算符的前置对象是cin和cout,不是类对象,因此不能将重载设计成类成员,而只能设计成普通函数,但是我们又想重载后的输入输出运算符读取类的私有数据,因此只能使用友元了

总的来说,重载输入输出运算符需要使用返回引用,引用参数和友元

在重载>>运算符时,函数原型中的对象参数不能用const关键字,否则会出错,因为>>运算符就是要往对象中输入数据,而const关键字是不允许修改对象中的数据,所以会出错

类和动态内存分配

1,静态类成员的特点是无论创建了多少对象,程序都只创建一个静态类变量副本,就是同一个类的所有对象共享一个静态成员,不能在类声明中初始化静态类成员,因为在类声明中只描述了如何分配内存,但实际上并不分配内存,可以在类声明以外使用单独的语句初始化静态类成员,因为静态类成员是单独存储的,而不是对象的组成部分。初始化放在方法文件中而不是在声明文件中,声明一般包含在头文件中,如果将初始化语句放在声明文件中,有可能会出现多个初始化声明

2,特殊成员函数可能会导致程序出现问题,这些成员函数是自动定义的

C++自动提供了下面这些成员函数

  • 默认构造函数,如果没有定义构造函数
  • 默认析构函数,如果没有定义
  • 复制构造函数,如果没有定义
  • 赋值运算符,如果没有定义
  • 地址运算符,如果没有定义
  • C++11新增了移动构造函数和移动赋值运算符

以下四种情况都会调用复制构造函数

#include <iostream>
using namespace std;
class student
{
	int age;
public:
	student() {};
};
int main()
{
	student stu1;
	student stu2(stu1);
	student stu3 = stu1;
	student stu4 = student(stu1);
	student *p = new student(stu1);
}

每当程序生成对象副本的时候,都会调用复制构造函数,当函数按值传递对象或者按值返回对象时都会使用复制构造函数,按值传递要调用复制构造函数,因此应该按引用传递对象,可以节省调用构造函数的时间和空间。默认的复制构造函数是逐个复制非静态成员,复制的是成员的值,成员复制也成为浅复制;要解决复制构造函数的这种问题是进行深度复制,定义一个显式的复制构造函数,如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据而不是指针,这是深度复制,浅复制只是复制了指针值而不会复制指针指向的信息。一般来说,假如在类中已经为数据成员分配了空间,那么在复制构造函数中不需要用new分配内存空间,只需要把数据复制过来;假如类中只是定义了指针,那么就要用new为数据分配新的内存空间,再将数据复制过来

重载赋值运算符的时候一般分为几个步骤,先判断是否为自我赋值,假如是自我赋值则直接返回this指针;如果不是自我赋值,则把参数中的对象的数据成员复制过来,再返回this指针

3,不提供构造函数时,C++将创建默认构造函数,默认构造函数使对象类似于一个常规的自动变量,它的值在初始化时是未知的,如果定义了构造函数,则C++不会提供默认的构造函数,如果希望在创建对象时不显式地初始化对象,则必须显式地定义默认构造函数,这种构造函数没有默认值,但可以设置对象中某些特定的值,带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值,但只能有一个默认构造函数

class student
{
	int age;
public:
	student() {};
	student(int n = 0) {};
};
int main()
{
	student stu;
}

以上情况会出错,不知道与哪一个构造函数进行匹配

4,正确地使用复制构造函数和赋值运算符,可以使类正确地管理类对象使用的内存

5,C++98中,字面值0有两个含义,既可以表示数字0,也可以表示空指针,NULL是一个表示空指针的C语言宏,C++11中引入了新关键字,用nullptr表示空指针,但0仍旧可以使用,建议使用nullptr

6,可以将成员函数声明为静态的,声明静态成员函数时函数声明必须包含关键字static,但是如果函数定义是独立的,则其定义不能包含关键字static。不能通过对象来调用静态成员函数,静态成员函数甚至不能使用this指针,如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。静态成员函数不与特定的对象相关联,所以它只能使用静态数据

7,在构造函数中使用new应该特别小心

  • 如果在构造函数中使用new来初始化指针成员,则应该在析构函数中使用delete
  • new和delete应该互相兼容,new对应于delete,new[]对应于delete[];如果有多个构造函数,则必须以相同的方式使用new,要么都带括号,要么都不带,因为只有一个析构函数,所有的构造函数都必须与它兼容,可以在一个构造函数中使用new初始化一个指针,而在另一个构造函数中将指针初始化为空,因为delete和delete[]都可以用于空指针
  • 应该定义一个复制构造函数,通过深度复制将一个对象初始化为另外一个对象,默认的复制构造函数通常是浅复制,浅复制容易出错,复制构造函数应该分配足够的空间来存储复制的数据,还应该更新所有受影响的静态类成员
  • 应该定义一个赋值运算符,通过深度复制将一个对象复制给另外一个对象,通常复制运算符应该完成这些操作,检查是否为自我赋值,释放成员指针以前指向的内存,复制数据的内容(深度复制)而不是复制数据的地址(浅复制),并返回一个指向调用对象的引用

8,有关返回对象的说明,当成员函数或独立的函数返回对象时,有几种方式可以选择,可以返回对象或者返回对象的引用;返回对象可以分为返回const对象或者非const对象,返回对象的引用可以分为返回指向const对象的引用和返回指向非const对象的引用

返回对象会调用复制构造函数,而返回引用则不会,所以当一个函数可以返回对象或者可以返回引用都可行时,应选择返回引用,因为其效率更高

返回对象的引用的函数原型要带上&;如果返回的对象是const对象,函数原型中也应该带上const关键字

当返回的对象没有公有的复制构造函数时,必须返回对象的引用,不能返回对象

总之,如果方法或函数要返回局部对象,则应该返回对象,而不是指向对象的引用,返回对象时会使用复制构造函数来生成返回的对象;如果函数要返回一个没有公有复制构造函数的对象,那么它必须返回指向这种对象的引用;当有些方法或者函数可以返回对象也可以返回指向对象的引用时,应该选择返回指向对象的引用,返回引用的效率更高

9,定位new运算符用于对象,使用定位new运算符为对象分配内存空间时要计算好对象的大小,每个对象的内存空间都需要不相同,否则新的对象会覆盖掉旧的对象,delete不会为定位new运算符创建的对象调用析构函数,这一点与new运算符创建的对象不一样,此时需要显式地调用析构函数,先调用定位new运算符创建的对象的析构函数,再delete分配的内存空间,销毁对象的顺序与创建对象的顺序相反

练习:

1,需要将数据存放到用new运算符新分配的内存中时,必须用新数据覆盖掉new分配的内存中的内容,因为new分配的内存中的数据是不确定的,如果不采用覆盖的方式将新数据存放到里面,可能会导致出错。比如讲一个字符串的内容存放到new分配的内存当中时,需要使用strcpy()覆盖内存中原有的内容,不要使用strcat()去拼接字符串

类继承

1,从一个类派生出另外一个类时,原始类称为基类,继承类称为派生类

公有派生,派生类对象包含基类对象,使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有方法和保护方法访问

2,派生类构造函数要点

  • 派生类的构造函数会先创建基类对象
  • 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
  • 派生类构造函数应初始化派生类新增的数据成员

创建派生类对象时,程序会先调用基类构造函数再调用派生类构造函数,基类构造函数负责初始化继承的数据成员,派生类的构造函数负责初始化新增的数据成员

3,基类和派生类之间的关系

  • 派生类对象可以使用基类的公有方法
  • 基类指针可以在不进行显式转换的情况下指向派生类对象,基类引用可以在不进行显式转换的情况下引用派生类对象,然而基类的指针和引用只能用于调用基类的方法,不能调用派生类的方法,C++要求引用和指针类型需要与赋给的类型匹配,但这一规则对继承来说是例外,这种例外是单向的,只可以将派生类的对象和地址赋给基类的引用和指针,不可以将基类的对象和地址赋给派生类的引用和指针。这种例外的单向性是有道理的,假如可以将基类的对象和地址赋给派生类的引用和指针,此时派生类的引用和指针可以调用派生类的方法,但对于基类的对象来说,它可能并不具有派生类中新增的数据,有可能会导致出错。上述的这个特点使得我们在使用基类的引用或者指针作为基类方法的参数时,我们不仅可以传递基类的对象和地址还可以传递派生类的对象和地址

4,多态公有继承指的是同一个方法在基类和派生类中具有不同的行为,实现多态公有继承的重要方法有两种

  • 在派生类中重新定义基类的方法
  • 使用虚方法

使用重新定义基类的方法时,程序会根据对象的类型来确定使用哪一个版本,假如通过基类对象来调用方法,则使用该方法的基类版本;假如通过派生类对象来调用方法,则使用该方法的派生类版本

虚方法在通过指针或者引用来调用方法时起作用,如果在多态公有继承中不使用关键字virtual,则程序会根据指针类型和引用类型来决定调用哪一种方法,如果使用了关键字virtual,则程序会根据引用或者指针指向的对象来确定调用的方法。因此在基类中将在派生类中重新定义的方法定义为虚方法,方法在基类中被声明为虚方法后,它在派生类中将自动成为虚方法

通常将基类的析构函数声明为虚方法,这样可以使得基类对象和派生类对象能够正确地调用析构函数

5,编译器决定在调用函数时程序将执行哪一个代码块,将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编,在编译过程中进行联编被称为静态联编,又称为早期联编,但是虚函数的出现使得有时使用哪一个函数是不能确定的,所以需要使用动态联编,又称为晚期联编

静态联编的效率比动态联编的效率更高,如果要在派生类中重新定义基类的方法,则将它设置为虚方法,否则,设置为非虚方法

虚函数的工作原理

编译器给每个对象添加一个隐藏成员,这个隐藏成员是一个指向地址数组的指针,这个地址数组被称为虚函数表,虚函数表中存储了该对象对应的类中声明的所有虚函数的地址。基类对象和派生类对象都有其独立的虚函数表,如果派生类重新定义了虚函数,则其虚函数表就保存了新的函数定义的地址;如果派生类没有重新定义虚函数,则其虚函数表中保存基类的该函数的地址;如果派生类新增了虚函数,则也将该函数的地址添加到虚函数表中

6,不能将构造函数声明为虚函数;析构函数应该为虚函数,除非类不用做基类,如果一个作为基类的类的析构函数不声明为虚函数,那么当使用基类的引用或者指针指向派生类对象时,会导致删除对象时执行的是基类的析构函数而不是派生类的析构函数;友元不能是虚函数,因为友元不是类成员;重新定义继承函数的方法不是重载,如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,所以重新定义的继承方法,应该确保与原来的原型完全相同,但如果返回的是基类的引用或指针,则可以修改为派生类的引用和指针,这种特性称为返回类型协变,这种特性只适用于返回值,不适用于参数;如果基类声明被重载了,则应在派生类中重新定义所有的基类版本

7,protect关键字,被声明为protect的数据可以被派生类直接访问,不能被类外直接访问,protect数据相当对派生类来说它是公有的,对于外部来说,它是私有的

8,编译器生成的特殊的成员函数

  • 默认构造函数,默认构造函数要么没有参数,要么所有参数都有默认值
  • 复制构造函数,当使用new来分配内存时,需要提供自定义的复制构造函数,这涉及到深复制和浅复制的问题,编译器提供的复制构造函数采用的是浅复制
  • 赋值运算符,如果需要采用自定义复制构造函数,那么也要提供自定义的赋值运算符

9,构造函数,析构函数和赋值运算符都不能被继承。派生类继承的方法的特征标与基类完全相同,但是赋值运算符的特征标随类而异,因为其包含一个类型为其所属类的形参

10,如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供一个赋值运算符,运算符的默认或隐式版本采用的是浅复制,如果对象属于派生类,那么将会采用基类的赋值运算符来处理派生对象中基类部分的赋值;派生类对象可以通过赋值运算符赋给基类对象,这时使用的是基类的赋值运算符,只处理基类中的数据成员,派生类中新增的数据成员将会被忽略;不可将基类对象赋给派生类对象,除非有相应的转换构造函数,因为此时使用的是派生类的赋值运算符,当中包含了对派生类中新增数据的处理,如果有相应的转换构造函数,那么将会临时通过这个转换构造函数构造一个临时的派生类对象,这个函数可以接受一个类型为基类的参数和其他参数,并且其他参数要有默认值;除了提供转换构造函数的方法之外,还可以定义一个用于将基类赋给派生类的赋值运算符

11,私有继承,使用私有继承,基类的公有成员和保护成员都将称为派生类的私有成员

各种继承属性
特征公有继承保护继承私有继承
公有成员派生类的公有成员派生类的保护成员派生类的私有成员
保护成员派生类的保护成员派生类的保护成员派生类的私有成员
私有成员只能通过基类接口访问只能通过基类接口访问只能通过基类接口访问
能否隐式向上转换

 

在类模板中使用友元重载运算符时,需要采用如下形式

template <typename T>
class Rect;
template <typename T>
ostream& operator<<(ostream&,const Rect<T>&);


template <typename T>
class Rect{
public:
   friend ostream& operator<< <T>(ostream&,const Rect<T>&);
};

该声明表示类模板的实例和它的友元之间是一种一对一的映射关系。

 将友元模板函数声明在类模板中,定义在类模板之外。当模板函数被声明为类模板的友元时,在函数名之后必须紧跟模板实参表,用来代表该友元声明指向函数模板的实例。否则友元函数会被解释为一个非模板函数,链接时无法解析。

使用类模板时要将类的定义,类方法的声明和定义放在同一个头文件中,不能放在单独的文件中实现。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值