C++知识点总结

基础部分

大杂烩

1、switch的case中定义的变量不能初始化,这时变量的作用域在整个switch内。注意定义string变量的时候,会隐式设初值,也不能定义。当使用{}作用时,可以设初值,但是作用域只在{}内。见primer 163页。

2、continue语句只能出现在for,while,do while语句内部,而switch语句时没有的。switch的case选项只能为整型常量表达式,而switch中可以是常量与变量。

3、++操作符要求操作的值为左值。i++返回时一个右值的副本,所以无法i++++,因为i++是右值。而++i但会自身的引用,是个左值,可以++++i。

4、对于内置变量,当在函数体外未初始化时(即全局),会被默认初始化为0,而在函数体内,则不会被初始化,此时的变量是未定义的。此处有一个特例,静态的局部内置类型也会被初始化为0。类内的变量可以显式初始化,如果没有则也是未定义的。例如在函数内部定义内置类型的数组,会导致数组含有未定义的值。

5、引用一个比较有趣的用法:const int &a=42;但是一定要声明为const。

6、三个const 容易搞混的做法:

const int *p=int const *p表示指针指向的对象的值是const,而指针本身可以变化;int * const p表示指针本身不能变化,但是指向的对象是int型的,可以变化。

7、c++内置的数组类型是无法拷贝的,也就是说它作为函数参数时是无法以值传递的,一般传递的都是数组的指针。

8、c++可以定义数组的引用:int (&arr)[10],小括号必不可少,不然成为了含有10个引用的数组了(实际引用的数组是不合法的),而中括号内的大小也必不可少。对于指针也是这样理解:

int *p[10]:10个指向int的指针       int (*p)[10]  一个指向含有10个整型元素的数组的指针

9、关于初始化。c++2.0定义了一种统一的初始化,即使用{}。A a{1,2,3};当编译器看到{T1,T2,T3…}的形式的时候,它会做出一个initializer_list<T>,其底部是由array<T>实现的,使用的时候,当被初始化的对象中的元素不是initializer_list类型,则{}所转化的initializer_list中的元素会被一个个拆开来进行传递。当容器自身所需的元素就是initializer_list类型,则不会被拆分。

10、{}可以用来初始化变量,同时{}初始化方法不允许变量窄化:

11、initializer_list里面的实现方法为包含了一个数组的指针以及数组的长度,而且没有实现深拷贝,所以对initializer_list对象进行复制只会是浅拷贝。

12、decltype(expression)返回的是表达式的类型。所以对于decltype一些令人难以想象的结果就可以有很好的解释

其实对于decltype(*p) decltype((a)),不太好理解其为int&,但是想:*p是个表达式,如果*p返回的是一个普通的int,那我们*p=2这样的操作就无法改变地址中的值,因为返回int的话就是做了一份拷贝,对原地址无法做出改变。然而实际是能改变的,所以其类型是int&。同理(a)咋看是个变量,其实是表达式,返回(a)的解果,而(a)返回引用。在decltype中传入函数,是不会执行函数中的程序的,所以不会真的计算函数的值,而是只会求得其返回类型,但是其还是需要传入实参的,即使没有用。

13、lambda表达式完整形式:

简单用法如下,其中 l 是一个对象,等号后面是对象的实现。当只有后面部分的时候,加上()可直接当成函数调用:

整理下完整的lambda表达式:

[]里面放置的是外界的变量,有三个很有趣的用法:

[=]表示所有的外部变量都是值捕获,由编译器推断使用哪个

[&]表示所有外部变量都是引用捕获,由编译器推断使用哪个

[&x]表示x以引用方式捕获,而不是传入x的地址。

()里面放函数的形参

mutable关键字其实是用于打破const的限制:

在此处的含义为:不加mutable表示外部的变量只是用来读取的,不能改变(值捕获),引用捕获的值可否改变取决于引用的对象是const还是非const,与是否加mutable无关。

->retType是尾置返回类型的写法(lambda必须这么写),{}是函数体,lambda表达式返回一个表示函数的对象。

lambda表达式可以定义在函数内部,这与其他的函数不同。当向函数传递一个lambda时,相当于创建了一个新类型以及对应的未命名对象。而被捕获的变量当成类的成员变量。

如果忽略返回类型,lambda表达式会自动推导返回类型,但是比较特殊。如果只有return语句。则类型根据return的语句定,如果有其他语句,返回void。

lambda不能有默认的参数。

lambda只有在其捕获列表中捕获它所在作用域的局部变量,它才能在lambda中使用。但是在这个局部作用域外的变量,又可以直接使用。总结来说,捕获列表只用于局部非静态变量,lambda可以直接使用局部静态变量以及它所在函数之外声明的名字。

lambda以值捕获的方式捕获的变量是在lambda被创建时拷贝,而不是调用时拷贝(与函数的默认参数的运行时拷贝不同,对应函数标签的第八条的代码)。当lambda作为函数返回值时,lambda不能包含引用捕获。这主要是因为局部变量不能以引用方式传出来。

[=],[&]的方式叫隐式捕获,当混合使用的时候,第一个必须是隐式捕获。而且前面使用引用捕获,后面就得用值捕获;前面值捕获,后面就得引用捕获。

lambda是一个函数对象,这个lambda的类型是特定的,之间把它当成函数指针来初始化时可以的,但是实际其类型应该是个编译器自定义的类, 对于lambda的捕获对象:如果是引用捕获,只要确保在使用lambda时引用所引的对象确实存在即可。而值捕获,相当于创建了一个成员变量,并在构造函数中指定了形参并传入。

lambda表达式产生的类不含默认构造函数、赋值运算符以及及默认析构函数;它是否含有默认拷贝/移动拷贝则通常视捕获数据成员类型而定。

14、c++基本类型char有三种,比较特殊:unsigned char ,char,signed char,而实际的表现形式是2种,char类型表现为另外两种中的一种。

15、signed char可以表现-128~127的数字,主要原因是计算机表现整型是用补码的方式,>=0的数,其补码就是原码,而负数,补码是除符号位按位取反后+1.所以对于原码为10000000这样的8位二进制,除符号位的补码为:1111111+1=128,加上符号位就是-128。

16、给无符号类型赋值超出其范围的数,最终结果为所赋的值对无符号类型数所能表达的整数个数取模后的余数。对于正数/负数的取模后续讨论。我总结出一个简化记忆,只有把所赋的值不断加上/减去无符号类型数所能表达的个数,直到到达范围内为止。而给有符号类型数赋值,超出范围此变量就是未定义的。

17、一个算数表达式中既有无符号类型,又有有符号类型,需要把有符号类型转换成无符号类型,转换方式和给无符号类型赋值一样,超出范围就加上/减去能表示的个数,直到满足为止。但是上述再实际运用中似乎只对int有效,应该是那些小于int表示范围的会在使用的时候往上转型为int。

18、临时对象属于右值。

19、右值引用,其实非常好理解,就是可以引用一个右值。但是在使用右值引用的时候,有很多地方很难理解:比如说,把一个临时对象传入一个以右值引用为参数的函数,我们知道右值引用目的是让右值中某些东西可以被复用,而不需要去拷贝它,但是临时对象在函数结束后就销毁了呀,那么那些复用的东西也要被销毁。这里就涉及比较复杂的东西,明确一点,临时对象在函数结束就被销毁是正确的。

20、0开头的字面值是8进制,0x是16进制。十进制字面值常量带符号,8/16进制不确定。10进制常量类型是int,long,long long中能放下的最小那个,而8/16进制是int,unsigned int,long, unsigned long,long long, unsigned long long中的最小那个。浮点数字面值常量基本类型是double。

21、当两个字符串字面值位置紧邻且仅由空格,缩进,换行符分割,那么它们就是一个整体:

22、转义字符可以用\x加上16进制数或\加上1,2或者3位的八进制数表示超过三个就忽略后面的,其中数值表示的是字符在字符集中的值,比如\n在Latin-1字符集上表示为\12。

23、对字面值常量改变默认类型:

字符与字符串前缀:

u   unicode16字符 -> char16_t

U   unicode32字符 -> char32_t

L   宽字符 -> wchar_t

u8 UTF-8(用于字符串字面值)  char

后缀(整型或浮点类型):

u/U unsigned

l/L long或者long double

ll/LL long long

f/F float

24、不同作用域的相同名字的变量,通过::访问全局:注意,这边不管有多少个作用域有相同的变量,只能访问最靠近自己这一个作用域里面的以及最外边的全局变量,而且全局变量必须是真的全局,而不是相对全局。

25、给引用类型的变量赋值不会发生拷贝,只是单纯的绑定,所以不会调用构造函数。

26、void*可以存放任意类型的指针。但是不能使用,因为不知道指向的是什么类型。任何指针类型可以转换为void*类型(不需要强制转换)。而void*转换为别的类型一定要强制转换。

27、把一个数值转换为字符串snprintf:

把右边…的数格式化为format形式,然后放入str中,如果右边的数最后格式化的大小<size,则全放入再加一个‘\0’,如果>=size,则放入前size-1个,然后加上‘\0’。

28、指针的引用int *p=NULL;int *&a=p;这样理解,首先与a最近的是&,表明a是引用类型,前面是int*,标明是int*类型的引用。

29、const与引用的对象一定要初始化,因为之后无法赋值。

30、了解下关于union的概念,union写法和结构体很像,但是union的成员变量是共享内存的,所以一个union的大小是其中大小最大的成员函数的大小。union里面不允许放引用,使用union往往是不同变量永远不会在同一时刻都有效的情况下。

31、引用的一个用法,value其实没被绑定,引用的是一个常量。

32、对const引用,只限制了这个引用能做的操作,而变量可以通过其他途径修改。

33、指针所指的对象必须在类型上对齐,不能隐式转换(其他的可以转为void*类型的,以及类继承关系下可以转换)。而引用是这样的,如果一个int型的引用绑定一个double,是把double拷贝成一个临时量,然后转为int型,再让引用绑定。所以引用绑定的是个常量。

34、常量表达式指编译过程中就能确定值的表达式,满足等号左边定义为常量,等号右边在编译中就能确定值。constexpr的作用是,它定义的变量一定是常量,而等号右边一定要是个常量表达式。不然编译失败。有点像override,主要靠编译器帮忙检查问题。constexpr与const有点不同,const int *p表示指针指向的是const int,constexpr int *p表示p是常量表达式。定义于函数体外的变量的地址不变,所以其地址是常量表达式,而函数体内的不是(除了静态变量)。

35、用类型别名定义的复合类型就是整体了,不能用直接文本替换的方式解读:

36、用auto推测类型的时候,首先会去引用,然后去顶层const,不过当定义为auto&的时候,顶层const会保留,即表明引用的那个对象是const的。特别的,对于数组,auto会推导为一个指针类型。但注意,定义位auto&的数组会返回数组类型,而非指针。

auto自动推导类型:

括号中的值有且只能有一个。

37、cin读取字符串与字符都是从第一个非空字符开始读取,直到下一个空字符(前面的空字符被丢弃,最后的空字符未被读取),所以cin无法读取空格、回车等字符。(空字符指空格,tab,回车)。而getline是除了回车之外,都会读取,即读取一行(回车被读取,并且丢弃)。scanf读取字符串从非空字符开始,直到遇到下一个空字符(前面的丢弃,最后的未读取)。与cin不同,scanf使用%c可以读取回车,空格等。getline返回istream类型的对象。其实cin的行为可以控制,对于是否读取空白格有个skipws与noskipws来控制,后续有讲到。

38、直接”hello”+”world”是非法的,+的左右两边必须至少一个string对象。

39、vector<int>v{10,10,10}表示v是包含3个元素的vector,当上述方法行不通时,会尝试套用构造函数,vector<string>v1{10,“hello”}表明v1有10个元素,每个都是“hello”。

40、在创建对象末尾加()会默认初始化对象值为0。

41、关于常量指针与指针常量,顺从网上的大部分说法:常量指针时指向常量的指针,指针常量时这个指针是个常量。

42、数组的类型一定要显式给定,不能使用auto。而且也不存在引用的数组。

43、假设数组有10个元素,用{}初始化时就给了3个,那么剩余的那些会被自动初始化为0。我所有地方说的初始化为0都是笼统的说法,对于指针是NULL,对于bool是false,等等等。

44、使用{}对字符数组初始化,’\0’一定要显式给定。

45、普通指针支持的运算和迭代器的运算一样。对于void*,支持其加上或减去一个常量表达式0。同时void* - vooid* = 0。

46、数组骚操作,只要指向的未超过数组的边界都可以这么干。但是vector这样的下标就不能是负的。

47、传入strlen,strcmp,strcpy,strcat的字符串必须以‘\0’结尾。

48、字符串的c_str()函数返回的类型是const char*,所以我们没法来改变这个值。当字符串改变值的时候,就算没有重新获取c_str(),返回的字符串的值也会变化(实验结果)。但是最好每次使用的时候重新获取一遍。

49、printf的%x  16进制输出 ,%o  8进制输出。

50、用范围for循环遍历数组的时候,除了最内层,其他层都要设定为引用,不然会被解析为指针,就无法继续内层的范围for循环了。(引用解析数组的时候还是数组类型)。

51、常量对象也是左值,但是无法放在=左侧。所以是一种特殊的左值。

52、int a; &a返回的是右值。

53、除了&&,||,?:,, 逗号这4个运算符规定了运算对象的求值顺序,其他都不规定求值顺序,所以自己的代码的运行结果只是在特定编译器上的结果。

54、算数运算符(+正 -负 * / % +加 -减)满足左结合律:

算数运算符的求值结果都是右值。

正号作用于指针返回指针的副本。负号不能作用于指针。

%只能作用于整数类型。对于m%n,符号永远跟m相同。也不用管什么正负号了。

55、逻辑运算符与关系运算符(!< <= > >= == != && ||),所有运算符返回值都是右值

只有!是有右合律,其他都是左结合律。

57、赋值运算符=:结果是一个左值,且为其左侧对象。赋值运算满足右结合律。

复合赋值运算:+= -= *= /= …

满足右结合,返回其左侧对象。

58、成员访问运算符-> .

也没说左结合还是右结合,盲猜左结合。箭头运算符返回左值,点运算符看成员,如果成员是左值,就是左值,如果成员是右值,就是右值。

59、条件运算符 ?:  :右结合律

当条件运算符后面两个值都是左值时,返回左值,否则返回右值。

60、位运算符  ~ << >> & ^ |

位运算符作用于整数类型。对于有符号的位运算符,操作都依赖于机器,而且<<是未定义的。

<< >>最终返回左侧对象处理之后的拷贝。且右侧对象一定是非负的,值要严格小于左侧对象的位数。这两个运算符满足左结合律。

对于<<,右侧插入0。对于>>,左侧对象是无符号时插入0,当有符号时,插入左侧符号位或者0,和具体的环境有关。

61、sizeof满足右结合律,返回一个size_t的常量表达式。

sizeof不会计算右侧的表达式。(后续有个typeid与dynamic_cast会出现一个运行时类型识别RTTI的术语,那会可能需要计算()中的值)。

所以int *p=nullptr;sizeof *p也是完全正确的。因为实际*p并没被计算。还有一种特殊情况:一般对类成员的访问需要对象,但是sizeof可以直接使用A::a来访问,而不需要具体的对象。,不过只对public的有效。

对于数组sizeof会得到数组所有元素占用大小的和,不会把数组名当成指针。

而对vector等只返回对象的大小,而对其指出去的那些元素大小不会计算入内。所以vector是3根指针形成的,所以是24个字节(64位电脑)。

62、逗号运算符

按照从左到右的求值顺序。其表达式返回的是右侧对象的值,所以右侧是左值,则返回左值;右侧是右值,则返回右值。

63、c++ volatile是让操作的变量看作程序外部的输入输出,而不能随意改变顺序和内容。volatile和多线程其实没关系,都是编译器自己加的功能而已。c++要对多线程操作,得用atomic类模板。对下面程序,编译器只会做打印工作,为了使得前面的程序也能被执行,就得加volatile。

64、类型转换:对于隐式转换,尽可能减少精度损失:

一般情况下小整数范围会变大。如果int能涵盖它所有的值,则变成int,否则就是unsigned int。而wchar_t,char16_t,char32_t会转成int ,unsigned int,long,unsigned long,long long,unsigned long long中最小的。

对于两个类型运算,会转换成较大的那个(这是在整型提升之后的再次提升)。如果操作数一个有符号,一个无符号。那个范围大转换成哪个,如果范围相同,转换成无符号。

还有一些隐式转换:数组与函数会隐式转换为指针。但是遇到decltype,&,typeid,sizeof则不会转换。

显式转换:cast-name<type>(expression);

如果type是引用,返回左值,其他我就试了个int转double的,返回右值。如果转成引用也需要int转成int&这样。

static_cast

用于不包含底层const的一系列转换。

const_cast:

只用于改变底层特性。如下的转换比较特别:使用指向变量的指针是无法改变真实值的,但是解引用指针却得到改变后的值:其实对象是常量,但指向它的指针强制转换为指向变量这种操作算是违规的,所以对这个指针赋值也是未定义的。一般是将指向变量的改为指向常量的。

reinterpret_cast:

为运算对象的位模式提供较低层次上的重新解释。

65、initializer_list对象中的值永远是常量值,我们无法修改。

66、内联函数给编译器一个建议,也就是在函数调用的地方用函数中的内容替代。编译器也可以忽略的。

67、IO类比如ostream属于不能被拷贝的类型,所以只能引用它们,而且返回类型不能为const,因为使用ostream就是为了在ostream中写入东西。

68、右值引用只能绑定到一个将要被销毁的对象或字面值常量(用const修饰的变量不行,因为const修饰的变量是左值,只是有点特殊)。右值引用只能绑定到右值上,不能绑定到左值上。(有个特例是模板类型的右值引用可以绑定左值,会将左值推断为&类型的)。

69、一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

70、右值引用一个令人惊讶的结果(实际b只是别名而已,但是这编译器怎么理解的,无从可知):

71、将变量使用move右值引用后,只能给变量赋值或者销毁,而不能使用变量的值。

72、正常情况下,我们不能是使用<等来定义指针,但是less<type*>良好定义了指针的这些操作,同时我们也可以自己写这些指针的比较操作。

73、枚举类型是字面值常量类型,有两种形式:限定作用域的和不限定作用域的。默认的枚举从0开始,依次加1。不限定作用域的枚举类型对象或成员可以隐式转化为int,而限定作用域的不行。

枚举类型是const的(确切说应该是constexpr,因为我自己测试中const的对象可以在运行过程中确定),所以初始化枚举成员的初始值必须是常量表达式。

74、我们可以为枚举类型成员设定潜在类型,如果实际值大于设定的类型,会报错。对于限定作用域的类型,默认成员类型是int,而不限定的没默认类型,只要能装下所有枚举类型。

75、可以前置声明enum,不限定作用域enum需要添加成员的默认大小,因此每个声明必须指定成员的大小。而限定作用域的可以不指定大小,值被隐式地定义为int。

76、枚举类型不能用同值的其他类型来匹配,但是可以将不限定类型的枚举对象赋值给整型。

77、穿插一个内存对齐问题,class,struct,union当产生一个变量的时候,会把前面的空间当作是自己大小的整数倍,所以会有内存对齐,使得前面的空间是自己大小的整数倍。

78、union可以指定为public,protected和private的。默认public.

union不能包含引用,它可以定义构造函数或析构函数等函数,但不能有虚函数。

79、union像类一样使用,可以使用{}初始化,但是初始化的是第一个值。

80、union可以定义为匿名union,然后其成员可以直接使用。

但是匿名union不能包含受保护与私有的成员,也不能定义成员函数。

union中包含类,要初始化类就必须调用类的构造函数,要把类换为另一个值,就必须使用析构函数。对于只有内置类型的union会合成拷贝与赋值操作。如果有类类型,且类定义了默认构造函数或拷贝控制成员,则编译器为union合成对应版本并设定为删除。有类的union一般放在类中易于管理。

函数

1、对于函数重载,顶层const之间时不能共存的。一般类型与引用之间可以共存,引用类型与右值引用之间可以共存。底层const能共存。

2、关于函数的重载,一般重载必须保证参数的类型或数量是不同的,返回类型可相同可不同。函数除去返回类型部分,叫做函数的签名。

3、关于return.如果函数的返回类型是void,可以使用return;也可以使用return xxx;此时xxx必须是返回void的函数。

4、函数调用中,返回为引用时这个返回值是左值,因为我们可以通过引用去改变值,其他都是右值,因为即使是指针,它也只是个变量,返回的时候返回一个常量值,总不能写一个等式:常量=一个值。以下可以成功运行,换成其他写法就不行。

5、几种返回数组指针的函数的写法:

6、关于普通函数的参数,虽然形参与实参对应,但是没有规定求值顺序,所以以下用法的时候要小心:

我的编译器顺序是从右向左求,而且是计算完之后才传的值。由于i++实际传的是一个i的副本,而++i传的是i的值,所以第一个表达式,先计算++i,i->1,再计算i++,相当于int temp_i=i(即temp_i=1) 再i=i+1->i=2,然后再传参数,所以最终结果为1,2。

函数是从右向左,直接用cout是从左向右。但是有一点不同,函数是计算完所有的实参再传参,而直接cout是计算一个传一个。所以结果挺奇怪的。

7、在内部作用域如果含有与外部作用域同名的函数时,内部作用域的函数会将外部作用域的函数隐藏。

8、c++函数有默认值的必须放在后面,不能一个默认,一个没默认这样的。可以用全局的变量作为默认实参,局部变量不行。当使用全局的变量设定默认值的时候,真实值得看调用那会的真实值。而由于绑定的值只看声明那会的作用域内的变量,之后就算覆盖 ,也是与绑定那个不是同一个。

9、函数多次声明可以给没设定默认值的变量设定默认值,设定了默认值的就不能再设了。

10、constexpr函数要求返回值与形参都得是字面值类型,而且必须有且只有一个return语句。

11、constexpr也可以不返回常量,所以都把我搞懵了,到底是需要常量还是不要常量。有这么个情况:总结感觉是只要传入的实参是常量表达式,然后函数体内部别有用到非常量表达式的,都可以返回常量。传入的其他变量要在运行时确定,而常量在编译时就确定了,所以在编译时传入常量返回的就是常量表达式。而传入其他的实参一定是在运行时才有用。(编译时能得出函数的值就是constexpr函数)。

12、内联函数和constexpr函数可以定义多次,但是每次定义都得完全一样。

13、对于有二义性的函数匹配来说,对应找出最匹配的,所谓最匹配就是隐式转换最少的。注意,对于小整数可以提升为int,所以如下:没有二义性,反而调用int。

14、函数的类型只与函数的返回值与形参决定,与函数名无关。

函数:void func(int,int)的类型是void(int,int)

所以函数指针类型是void(*)(int,int)

定义一个函数指针:void(*p)(int,int) p是个函数指针。

函数指针一定要与对应的函数精确匹配,隐式转换不管用了。这就是指针的特性,必须精确匹配。除了void*与子类向基类的转换。

15、作为形参,没有函数类型的形参,就算写成函数类型的形参,会自动转化为函数指针类型的形参。

对于函数返回类型为函数指针的情况,如果要写全而不用auto等操作时,一定要写成函数指针的形式,不能写成函数类型。

可以用类型别名等方法,也可以这样:

int (*func(int))(int,int)表示函数func的返回类型是int(*)(int,int)的函数指针类型。

或者auto func(int)->int(*)(int,int)。

16、声明但不定义一个函数是合法的,但是不能使用它。

17、对于有二义性的类型转换,一定要显式调用,而不能使用强制转换。因为强制转换也存在二义性。

18、对于二义性,只要无法精确匹配,就算有一个差的远,一个比较接近,都没有太大的优劣。但是注意算数值的会提升为int.

19、还有一个点,在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换不止一个,则还是具有二义性。对于所谓的调用同一个类型转换,我暂时想不出例子,不过情况应该和上图差不多吧。

20、对于函数,可以通过尾置类型加上decltype获得返回类型,而想要去除引用,用remove_reference:

remove_reference<int&>::type为int。

普通类

一般规则

1、成员访问权限:public:类内与类外以及友元都可以访问;protected:类内,子类,友元都可以访问;private:类内,友元可以访问。

2、const对象不能调用类内没加const的函数。

3、操作符重载可以写在类外,这时候时没有this指针的。有些重载比如<<操作符作为流输出时必须写在类外,所以必要的时候就得定义为友元。

4、类的构造函数如果没加explicit,外部基本类型的变量如果符合其单一参数构造函数的参数,会隐式转换为该类的类型。explicit只能放在类内,就算构造函数的定义在类外,也不行。被explicit修饰的构造函数只能使用()调用,什么意思呢,看下面代码解释。不过explicit只是阻止了隐式转换,对于显式转换还是可以执行的:比如static_cast<A>(2);

对于通过一个实参调用的构造函数只能是一步转换:这种转换生成一个临时变量进行传递,所以传递右值。

5、在函数最后面加const也算签名的一部分,所以就算其他都一样,一个有const,一个没有const,也能同时存在。而且,对于一个类有这两个版本的时候,常量(const)对象只能调用const版本,非常量(non-const)对象只能调用non-const版本。

6、noexcept放在函数后面,还可以加条件,表示此函数一定不丢出异常。使用如下:vector的移动拷贝构造与移动拷贝赋值函数一定要加noexcept。

7、final关键字,修饰类的时候表明类不能被继承,修饰虚函数表示虚函数不能被重写。

8、成员函数声明必须在类的内部,而定义可以在类的外部。使用::。

9、定义类内部的函数是隐式inline的。(注意是要在类内定义,如果只是在类内声明了一下就需要显式声明成inline,或在类外定义的时候写成inline)。

10、类中this指针是个常量,不允许改变。

11、类内函数末尾加const实际是修改了this指针的底层const,构造函数不能声明为const,所以当创建const对象时,是在构造完成之后,对象才得到其常量属性。如果对一个const函数返回引用为*this,则返回的将是一个const A&的类型。

12、因为编译器在编译过程中先编译成员声明,再编译成员函数体(注意是函数体),所以类中的变量放在函数之后也没关系(函数体要被使用到才会初始化)。而typedef需要先定义后使用。

13、对于=default可以出现在类内,也可以出现在类外的定义之后。如果在类内,则构造函数是内联的,如果在外,默认不内联。

14、编译器会为我们合成默认构造,拷贝构造,拷贝赋值,析构。

15、struct默认访问权限是public,而class默认是private。

16、类内变量执行类内初始值的时候,只能使用=或者{},不能直接利用构造函数初始化。

至于为什么不能这么干,网上说是怕这种情况:

我觉得编译器能够分清到底是变量还是类型,应该是担心程序员自己搞混。

17、类类型的变量可以A a;这样定义,也可以class A a;这样。第二种是之前没见到过的。

18、自定义的类可以只声明而不定义,这叫前向声明。但是用处非常有限。只能用这个声明定义指针与引用类型。也可以在函数上声明上作为返回值类型或形参类型(注意,返回值或形参为一般类的类型时,函数不能定义,不过作为引用与指针时可以的)。所以在真正使用这个类之前,一定要定义过,不然类占多少空间不得而知,导致内存分配不得而知。

在类内包含自身的指针以引用是可以的,但不能包含简单的对象成员。但是成员函数的返回值以及参数以及函数体都可以直接包含对象并使用。关于成员函数都能用的原因,是因为首先函数声明的时候,是可以用类名的,而函数体的编译是在类完全可见之后,所以函数体内也可以用类名。而对于一般函数,无法保证编译函数的时候类已经被编译完全。对于类成员无法定义一般对象类型,也可用相同的解释。当然还是因为,如果能创建对象,则对象里还有对象,递归创建会导致堆栈溢出。因为这种创建是会分配内存的。而使用指针或引用没这种问题。

19、类的成员函数的函数体是在整个类可见之后才编译,所以成员函数能使用任意在类中定义的名字。我想在此总结下,一般的声明在声明之后就能用了,但是友元函数和类只是声明是不行的。

20、类中,如果类外定义了一个代表类型的名字,而类内先用了这个名字,然后再重新定义这个名字,是出错的。不能重新定义,就算一模一样(我的电脑可以这样执行,但是记成不行吧)。而普通情况可以这样操作:

对于以上的typedef,会在用到I的前面语句中查找,没有再从全局查找。成员函数中的查找如下:先从成员函数自身查找,没有就从类的所有成员中找,因为函数体编译在类成员后,所以此时外部的所有成员都是可见的。再没有就在成员函数定义之前的作用域找。(定义在类外的情况)。

21、对于类的构造函数之后的初始值列表,是进行初始化,仔细对比下面两种写法的区别:

而const成员,引用成员或没有默认构造函数的类型必须在初始值列表中初始化或类内初始化(就是直接给个值)。而列表初始化的顺序是根据变量在类中出现的顺序定义的。

22、声明并使用默认构造函数初始化一个类的对象应该这样写:A a;而不能A a();因为第二个式子让人分不清是对象还是定义了一个返回A类型的函数。

23、构造函数与析构函数作用的对象都是非静态成员。析构函数是唯一的,不能重载。析构的顺序与构造的顺序相反。当指向一个对象的引用或指针离开作用域时,析构函数不会执行(一定要是对象离开作用域才行)。

24、=default可以写在类外,而=delete必须写在第一次函数声明的时候:

=default只能用于有合成版本的函数,而=delete可以用于任意函数。

析构函数不能是=delete的。写法上可以,只能为这类对象动态分配内存,但是无法删除。

25、在成员函数后加const,&等一定要声明与定义的时候都写出来。而且同时两个都有的话一定要先写const,再写&。对于&的含义,表示this是引用的类型的。又如,如果定义为&&,表示this是右值引用类型,当外部对象是右值且调用函数就是调用这个版本。当定义了两个即以上参数列表等相同,只有引用限定符不同的函数时,每个函数都要加上引用限定符。

26、struct的成员默认是public的,而class是默认是private的,而且struct的继承默认也是public的,class是private的。除此之外,struct与class根本没有区别了。

27、如果一个类定义了析构函数,即使是=default的,编译器也不会为这个类合成移动操作。

28、数据成员指针:

string A::*p=&A::s;表示指向A中s的string的指针。此指针只是说指向类的某个成员,而没指定类的具体对象。使用的时候就是把指针解引用当成类的成员,然后用类的对象或指针去访问。

注意看两个操作符.*以及->*。

对于数据成员的访问呢和普通规则一样,public哪都能访问,protected友元、子类、类内…

所以有时候让类返回数据成员指针。

29、可以定义成员函数指针,和定义数据成员一样。注意:成员函数和指向成员函数的指针之间不存在自动转换规则。使用的方式也差不多。

30、成员函数指针都不是可调用对象,需要使用function 转换为一个可调用对象,而且需要把this参数显示表示:

也可是使用mem_fn从成员函数指针直接生成一个可调用对象。

mem_fn生成的可调用对象有重载,一个是引用类型,一个指针类型。

还可以使用bind生成可调用对象:

31.this指针在成员函数开始i前构造,在成员函数结束后清除,说白了就是成员函数的一个参数。

友元

1、在类内声明友元需要加friend,当在类外定义时一定不能加friend。

2、友元不受public,private,protected的限制,外部都可以用。友元在类内部声明后,要在外部再次声明才能被外部使用。

3、友元在类内定义的时候是内联的,对于友元,没有this指针,因为就不属于类的成员。

4、友元可以是其他类或其他类的内部成员函数。

5、友元不存在传递性。

6、特别规则:友元声明和普通的声明不一样,它只是对访问权限的控制,实际使用到友元的地方必须真正定义过之后。

7、同个类的各个对象互为友元,也就是说在对象内可以直接使用另一个对象的private.protected,public成员。

静态成员

1、static关键字在声明的时候需要添加,声明之后在实现部分一定不能加。

2、静态成员的访问限定也遵循基本访问规则,比如private的静态成员在子类是访问不到的。

3、静态的成员函数只能访问静态的成员变量,因为它没有this指针。一般外部访问静态成员需要 class::member的方法,但是静态函数调用静态成员不需要。

4、类的静态成员函数可以在类的内部与外部定义,而静态成员变量只能在类的外部定义(除非静态成员变量是字面值常量类型的constexpr)。

如果这个静态变量仅在类内使用,类外不需要定义了(注意这边叫定义)。如果需要在外面使用,需要定义(有些情况不需要,如果作为引用的参数,是需要的,那些不需要,我也不知道),但是如果类内有初始值,则类外不能有初始值了。

静态成员类型可以是所属类的类型,而一般成员只能是所属类指针或引用。静态成员还可以作为成员函数的默认实参。非静态成员不行。

操作符重载与强制转换

1、对于一个类,可以定义其强制转换的类型,写法如下,注意返回类型没有,而且没参数:

2、对于++、--的重载,前置++、--没参数,后置++、--有一个int的参数。

3、一个很吃脑子的东西,比如,你在一个类A里面重载了=与*这个符号,然后类A里面有A a=*this。先调用的是=操作符,使得后面的*this被当成参数传递给=操作符。所以重载的*操作符不会被调用。

4、操作符重载中:除了()之外,其他的操作符不能含有默认的实参。

5、类内的操作符左边是隐式的this指针,但是注意,在使用的时候外部是以对象的方式使用的,而不是指针:

6、对与有求值顺序的运算符:&& || , ?:来说,?:不能被重载,其他三个可以重载,但是求值顺序规则无法被保留,而且对于&& ||来说,操作符两边的表达式都要被计算。有一个操作符需要注意,就是->运算符。我们重载它的时候不能丢弃它获取成员这一特性。

7、当我们重载输入运算符>>的时候,必须处理可能失败的情况,而输出运算符不需要。

8、赋值(=)、下标([])、调用(())、成员访问箭头(->)运算符必须是成员。

9、总结下->运算符:

对于型如point->mem的表达式来说,point必须指向类对象的指针或是一个重载了operator->的类对象。根据point类型不同,point->mem有不同的含义:

(*point).mem                //point是一个内置类型的指针

point.operator()->mem   //point是个类的对象

如果是指针,就是直接取类内元素。

如果是对象,就是调用操作符重载,重载的操作符必须返回一个重载了->的对象或指针。然后再将返回的对象或指针用户->解析。

10、对于类自定义转换的类型,不能是数组与函数类型。函数无法返回数组类型与函数类型。类型转换运算符必须定义为成员函数。

11、自定义类型转换设置成explicit的时候,一般的隐式转换会被禁止,但是如果被用作条件,则会自动调用显式转换:

if、while、do的条件部分

for语句开头条件表达式

逻辑非运算符,逻辑或运算符,逻辑与运算符对象

条件运算符?:的条件表达式

12、使用重载运算符与类型转换也会导致二义性。特别是两条路都可而且没有哪条有明显的优势。

委托构造函数

1、一个新概念:委托构造函数。指的是利用类的其他构造函数作为初始值列表的入口。而且只能有一个入口。先执行被委托的构造函数的初始值列表与函数体,再返回委托者的函数体进行执行。

拷贝构造函数 | 拷贝赋值操作符 | 移动构造 | 移动赋值

1、拷贝构造函数不能有多个,既然是拷贝构造函数,那么它的第一个参数是自身的引用,而其他参数都有默认值,所以相当于只有这一种形式叫拷贝构造函数。

2、拷贝构造函数定义:第一个参数是自身类类型的引用,任何额外参数都有默认的值。默认拷贝构造函数对数组的拷贝是逐元素拷贝。因为数组不能被拷贝。对成员类对象的初始化是使用类对象的拷贝构造函数。拷贝初始化在使用花括号初始化数组或聚合类成员时会被使用。编译器有时候可以略过拷贝构造函数(不知道怎么略过,书上写的这个):

3、赋值运算符通常应该返回一个指向其左侧运算对象的引用。合成的拷贝赋值函数对成员的拷贝是调用成员类型的拷贝赋值运算符完成。对于数组类型的对象,逐个赋值数组元素。

4、移动构造函数与移动赋值操作符必须是noexcept的,因为如果在移动的过程中发生了异常,则原来被移动的对象已经被改变了,无法保证异常安全,而一般的拷贝构造什么的,发生异常时是不会对原对象有影响,为了在异常发生时对自身行为做出保障,干脆就不产生异常,所以要加noexcept。

5、编译器可以生成合成的以移动构造、移动赋值。但是如果一个类定义了自己的拷贝构造、拷贝赋值或析构函数,则不会合成。如果类没有自己定义拷贝控制成员,且每个非静态成员都是可移动的(内置类型或包含移动操作的类),才会合成移动版本。

6、如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。(等面经上出现这些再记忆得了)例如:

成员有删除或无法访问的析构函数,则类的合成构造、合成析构与合成拷贝构造定义为删除的。

成员有拷贝赋值被删除或不可访问,则类的合成拷贝赋值定义为删除。

成员有拷贝构造删除或不可访问,类的拷贝构造就定义为删除的。

成员有const或引用的,则默认的拷贝赋值定义为删除(拷贝构造可能由于能够使用初始化列表才没被包含进去)。当const与引用没有类内初始化器的时候,默认的构造函数也定义为删除的。

7、移动操作不会隐式地定义为删除,除非我们显式定义为=default。除了这个意外,还有一系列原则:(等真的需要记的时候再记吧)

当定义了拷贝构造,或者没定义拷贝构造,但是编译器无法合成移动构造,则定义为delete。拷贝赋值同理

如果类成员类成员的移动操作有delete的或不可访问的,则设为delete。

若类的析构函数是delete或不可访问的,则设为delete

类成员是const或引用,则一佛那个赋值是delete的。

8、如果一个类定义了移动构造或移动赋值。则合成的拷贝构造和拷贝赋值是delete的。

9、有个情况:只要显式声明了函数,不管是有效或无效,都会调用那个最匹配的版本:

继承体系下的类

聚合类 | 字面值常量类 | 嵌套类 | 局部类

1、所有成员都是public;没有定义构造函数;没有类内初始值;没有基类与虚函数。

这样的聚合类可以使用{}来给成员初始化(其他的类需要符合构造函数的形参),顺序与成员定义的顺序一样。如过{}给定的值少于变量个数,则剩下的默认初始化为0。这个0代表很多含义,和类型有关。

2、有一种类为字面值常量类:成员都是字面值类型的聚合类是字面值常量类。

或者数据成员都是字面值类型,且至少有一个constexpr构造函数,且类内初始值都是常量表达式,如果成员是类类型,则初始化要是用它自身的constexpr构造函数,且析构函数得是默认的。constexpr构造函数必须初始化所有的成员,参数必须是常量表达式或constexpr构造函数。

3、嵌套类:完全可以把嵌套类当成一个成员。其在外部的访问权限也和普通的成员一样。嵌套类的成员可以定义在外部:

感觉不管是成员还是还是其他的,都是在外面多加一个作用域而已。

外层类和嵌套类完全无关,只是嵌套类的访问权限有点变化而已。

4、类可以定义在函数的内部,叫做局部类,之前有个定义在类内部的叫嵌套类。但是与嵌套类不同,局部类的成员必须完整定义在类的内部。

局部类里面不能有静态成员。

局部类只能访问外层作用域的类型名,静态变量以及枚举成员。也可以访问全局成员。

其名字查找也是从内到外。

在局部类中嵌套类,嵌套类的定义最多在和局部类相同的作用域。局部类的嵌套类也是局部类。

一般规则

1、子类定义与父类相同名字的变量时,子类的名字隐藏父类的名字,对于函数与变量都是如此。如果没覆盖父类名字且子类有访问权限,则直接访问即可。如果覆盖了父类的名字且有访问权限,使用Base::member方式访问。

2.继承关系下成员的访问权限

下面定义一下访问的概念:

一是派生类中的新增成员访问从基类继承的成员,这属于在派生类的内部进行访问。

二是在派生类的外部(非类族内的成员),通过派生类的对象访问从基类继承的成员。

当类的继承方式为公有继承时,基类的公有成员和保护成员被继承到派生类中其访问属性不变,仍然作为派生类的公有成员和保护成员,而基类的私有成员不可直接访问。注意,受保护的成员只能在类的自身函数内部被访问,不可以通过对象直接访问。

当类的继承方式为保护继承时,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可以被直接访问。这就是说,我们只能在派生类的成员函数内部读取或修改基类的保护成员与公有成员,而基类的私有成员无法通过派生类进行任何操作。

当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员的身份出现在派生类中,而基类的私有成员不可直接访问。

有个情况说明一下,以便搞混:

3.子类可以改变从基类继承来的变量在外部的访问权限,前提是其可以在子类被访问,所以永远无法改变private的访问权限。改变权限的方法为using Base::member。以下代码可正确执行。

4、继承体系中,只要类里面有虚函数,就会多一根虚指针,不管虚函数数量是多少,永远都是一根虚指针。虚指针指向虚表,虚表里面存的是函数指针,对应子类父类定义的虚函数。通过这种方式完成多态绑定。

5、在一个对象中,继承自基类的部分与派生类的对象在内存中不一定是连续存储的。

6、从派生类初始化基类成员需要使用基类的构造函数。是在构造函数的初始化列表进行。初始化顺序是先初始化基类部分,派生类部分按照声明顺序来初始化。

7、派生类的声明不需要加类派生列表.只要直接class Derived就好了。

8、一个类作为基类一定要是已经定义了。很明显,如果未定义的话其成员在派生类中就不得而知,所以会造成问题。

9、我们可以把一个基类的指针或引用绑定到派生类。智能指针也支持派生类向基类的转换,也就是说能够把派生类对象的指针存在基类的智能指针中。

10、静态类型:编译时已知的类型,为声明时的类型。动态类型:内存中真实的类型,运行时才知道。如果表达式既不是引用也不是指针,则其静态类型与动态类型永远一致(永远是都是静态类型)。

11、派生类能向基类转换,基类不能向派生类转换(即使基类绑定的对象是派生类)。在使用dynamic_cast的时候才可以转换。

12、只有是指针或引用时,派生类才能向基类自动类型转换,其他时候会对象切除。

13、派生类只能它的直接基类。如果A继承B,B继承C,则A只能初始化B。

14、继承中的类作用域是派生类嵌套在基类之内的。如果派生类对象调用了一个函数,在派生类中没找到,就往基类找,直到找到为止。

15、当一个基类的构造函数有多个默认的实参,继承的基类不会继承这些实参。相反,它会生成多个构造函数,每个分别省略掉一个含有默认实参的形参。而且我们使用using在子类中声明基类的构造函数,只是继承了子类的构造函数,而不会改变子类构造函数在子类中的访问性。比如本来应该是private的访问级别,我们在public:下声明的using,访问级别还是private.只是生成了一个public的参数和基类相同的构造函数。

16、一个using语句不能指定explicit或constexpr。如果基类的构造函数是explicit或constexpr的,则继承的构造函数也拥有相同的属性。

17、当一个类只有继承的构造函数时(这些继承的构造函数不会被作为用户定义的构造函数来使用),它也会拥有一个合成的默认构造函数。

18、对于多重继承,基类的构造顺序与其在继承的时候出现的顺序一致。

多继承可以继承多个基类的构造函数,如果出现相同的构造函数,则需要定义自己的版本才不会冲突。

对于合成的拷贝和赋值,多继承会隐式的调用基类的拷贝构造,一层层往里传。拷贝赋值类似的道理。

19、派生类的指针或引用可以转化为任意基类的指针或引用。其真正能使用的成员还得看它的静态类型。

20、派生类的作用域嵌套在直接基类和间接基类的作用域中,查找过程沿着继承体系向上进行,直到找到所需名字。基类的查找是并行的,所以不能有冲突(一个基类表示其直接基类与间接基类)。但是定义成相同的无所谓,只要使用的时候指出具体的基类即可。注意,即使冲突的函数在某个类中是私有的,也不行。

21、类可以多次继承一个基类,但是派生列表中不能多次写,是通过间接的一些方式实现的。在默认情况下,派生类会包含多份基类的子对象。然而可以使用虚继承使得只有一份拷贝。

22、因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并且不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,则我们仍然可以直接访问被覆盖的成员。但是如果成员被多于一个基类覆盖,则一般派生类必须为该成员自定义一个新的版本。

在只有一条派生类路径覆盖的情况下,直接基类的优先级比间接基类的优先级更高。

23、对于虚继承下基类的初始化,最底层的类,比如上述的D能够直接初始化A,这样就防止了嵌套初始化时的重复初始化。其初始化方式为先初始化虚基类,然后初始化直接基类,按照派生列表来。如果有多个虚基类,还是虚基类先初始化,合成的拷贝构造、移动构造、拷贝赋值、移动赋值也按照这个顺序。

多态特性

1、使用非指针或引用方式实现多态时,会出现类型转换的问题,称为对象切割。而指针与引用只是对地址以及大小的记录,所以会调用到多态机制。还有,如果正确地用了指针或引用,但是却没有设定为虚函数,那就是称为静态联编,还是调用静态类型的对应函数。

2、对于虚函数,只要基类加上关键字virtual即可,子类可不加,为了确定子类确实是重写了基类,在函数参数列后,函数体前加上override关键字即可。final和override是放在形参列表(包括任何const或引用修饰符)和尾置返回类型之后的。

3、对于父类的析构函数,一定要设定为虚函数。如果不是虚函数,就不会触发多态绑定,从而可能导致内存泄漏(如果子类析构函数定义了释放某些内存,而我们析构的是个父类的指针,那就无法动态绑定到子类的析构函数)。

4、成员函数如果没有被声明为虚函数,则其解析是在编译时,而不是运行时。

5、派生类必须在其内部对所有重新定义的虚函数进行声明(废话一句,要重写肯定要重新声明),而派生类的virtual可加可不加。

6、即使虚函数在基类是私有的,也能在派生类重写:

7、虚函数可以有默认实参,如果某次调用使用了默认实参,则实参值由静态类型决定(即使通过引用或指针调用的)。

8、我们也可以回避虚函数的动态绑定机制,只要显式使用作用域运算符调用即可。

9、类的虚函数的=0只能出现在类内的语句声明处。定义了=0的纯虚函数可以有函数体,但是必须在类外定义(即使定义了函数体还是抽象类,还是无法定义对象),如果有纯虚函数,就不能定义此类的对象。不过指针和引用是可以的。

10、一般情况下。派生类重写的虚函数要与基类完全一致,除了返回类型是基类的指针或引用时,规则无效。如果D由B派生而来,则基类返回B*,派生类可以返回D*。只不过要求从D到B的类型转换是可访问的(见11条)。

11、关于派生类向基类转换的可访问性:

如果是用户代码,一定要公有继承能用派生类到基类的转换。

如果是派生类内部,不管什么继承方式都能使用转换。

如果是派生类的派生类,只有公有继承或保护继承可以。

总结来说:在代码的某个节点中,如果基类的公有成员是能访问的,则派生类向基类的类型转换也是可访问的。(比如类外要用转换,则派生类肯定要保证属于基类的成员可访问,所以只能public继承,而在派生类的类内,不管声明继承都能访问基类的公有成员。而派生类的派生类要能访问到基类的公有成员,就得保证派生类中的成员是公有或保护的,则基类到派生类为公有或保护继承都行。)

12、派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类的成员和友元只能访问派生类对象中的基类部分的受保护部分。(13条有解释)

13、友元在继承关系中的有趣的部分:

首先,每个类的友元只负责对此类中的成员访问(这句话很容易理解错误,其真实含义是,这个友元函数,只要碰到此类的变量,都能访问,就算这个类被继承了,友元也能访问派生类中的基类部分)。而派生类如果来个友元,也只能访问派生类中自己定义的那部分,继承而来的那部分的访问权限和其他普通类内成员的访问权限一样一样的。

14、一个对象、引用指针的静态类型决定了对象那些成员是可见的,而且只与静态类型相关,与动态类型无关。

15、名字查找与继承:

对于调用p->mem()或者obj.mem,先从静态类型的作用域查找,一层层往外找直到找到为止。

找到之后判断调用是否合法。

如果合法,则看是否是虚函数且是否通过指针或引用调用,如果是就在运行时动态绑定,如果不是则生成常规函数调用。

16、在派生类中定义与基类同名的函数,即使形参列表不一样,也会隐藏基类的函数。因为在内层找到了,就不会继续查找了。(言外之意是继承下来的就像在外层作用域一样,里层的定义就隐藏,有点像普通的全局与局部的关系)。

17、如果一个基类有很多个名字相同的重载函数,而派生类想覆盖一个函数,其他还得不被隐藏,就可以使用using声明想要的函数名(注意,函数名的所有版本得在派生类能访问),则所以都在派生类作用域中了。然后再写想要覆盖的版本就好了(普通的using声明不允许覆盖,即不能再定义一个相同名字相同参数的函数,但是类继承中可以,对于变量,类继承也是不可以):

18、当构造函数与析构函数调用虚函数的时候,只能调用属于自己的那一个。(即在构造函数和析构函数中不具有多态性)。

静态成员

1、对于含有静态变量的父类,其子类也共享这个静态变量,而不会创建一个新的静态变量副本。当然子类也可以创建相同的名字的静态变量或一般变量,那么对于父类静态变量的访问就使用Base::member这种方式。

拷贝控制 | 移动控制

1、具有继承体系的类合成的拷贝控制操作只管理自身的变量,同时还会调用直接基类的同类操作去初始化基类的成员。所以这些合成的操作和友元有点像,只管当前类新出现的东西。什么析构啊都只管理自己的部分。

2、如果一个类不能执行或没有移动操作,则其派生类也不能合成默认的移动操作。

又是一波大规则:(面经有的话再记一下)

如果基类中的默认构造函数、拷贝构造函数。拷贝赋值运算符或析构函数是被删除的函数或不可访问的则派生类中对应的成员将被删除,原因是编译器无法使用基类成员来执行派生类对象基类部分的构造、赋值、或销毁。

如果再基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。

如果基类的移动操作时删除或不可访问的,则派生类该函数时删除的,原因是不可移动基类的部分。如果基类析构函数时删除或不可访问的,则派生类的移动构造也是被删除的。

3、在默认情况下:基类默认构造函数初始化派生类对象的基类部分。如果想拷贝基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝构造函数。

和派生类的拷贝构造一样,派生类的赋值运算符也要显式调用基类的赋值运算符。

4、类不能继承默认、拷贝和移动构造函数。但是对于其他的构造函数,在基类中使用using Base::Base;就行。通常情况下,using声明语句只是令某个名字在当前作用域内可见。而当作用于构造函数时,using声明语句将令编译器产生代码,对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数:derived(parm):Base(args)。其中的形参列表完全相同。

STL标准模板库

1、容器中

size_type  能放下所有元素的最大容器

value_type  元素的类型

reference/const_reference  引用与const引用

对于stack、queue、priority_queue还有一个container_type,表征实现此适配器的底层容器类型

2、在对const容器调用begin和end时才会返回const版本。

3、当使用一个容器初始化另一个容器时,容器的类型与元素类型必须匹配。但是如果时用迭代器范围初始化时,容器类型不一定要匹配,甚至元素类型只要能隐式转换即可。

4、顺序容器有assign函数,传入的迭代器不能指向调用assign的容器,即不能自己赋值自己。

5、emplace_back,emplace,emplace_front分别对应push_back,insert,push_front。emplace系列函数是把容器储存的元素的元素类型的构造函数的参数传递进去,使得其内部进行构造,而非传递对象。

6、容器的front,back以及at函数返回的是引用值,所以可以通过这个值直接改变元素的值。

7、容器的resize(n,val=0)函数,表示把容器大小改为n,如果大小变大了,则在末尾插入val,如果大小变小了,则删除后面多余的那些。resize不改变capacity,只改变size(当然resize之后如果比当前capacity大的话肯定会改变)。如果需要退回空间,有个shrink_to_fit()可以建议退回多余空间,但真实会不会就不知道了(调用之后又是重新分配内存了)。

8、容器的reserve函数允许我们通知容器它应该准备保存多少个元素。

9、对有序的关联式容器的key放入自定义的类时,必须严格弱序。最主要是两个对象a,b,当a<=b时,b不能<=a;

10、旧c++标准的容器中保存的类必须是可拷贝的,而新c++标准中,容器中保存的类可以不能被拷贝,只要能被移动。 

string

1、string内部只有一根指针,指针指向一个内存区,所以就单纯string对象来说,它的大小只是4个字节。还有就是,string只是个typedef,它的真正名字是basic_string<char>。

2、字符串初始化的好方法:

还有个substr函数,给定起始下标或者下表加长度:

如果substr开始为止加上计数值大于字符串大小,则自动调整为到末尾。

append以及replace用法

3、c++的查找会返回一个下标,如果没找到,会返回string::npos,其值是最大元素数量。

find(args)  找第一个出现args的位置

rfind(args)   找最后一个出现args的位置

find_first_of(args) 找第一个出现的args中的任意字符

find_last_of(args)  最后一个出现args中的任意字符

find_first_not_of(args)  第一个不属于args中的任意字符

find_last_not_of(args) 最后一个不属于args中的任意字符

array

1、关于array,前面其实已经提到过了,那是利用数组实现的,所以一个数组以及数组的长度。array的迭代器也是直接使用普通的指针,而没有繁杂的类的设计。 而且没有构造函数以及析构函数。这边有个地方要注意:array的end函数返回最后一个元素的下一个元素,但是超出了数组的界限,虽然超出了界限,我们可以发现代码可以通过编译,只是我们别去修改那个值,不然会出无法预知的问题。

2、关于数组的一些小知识:

3、如果array的元素是一个类,则这个类一定要有默认构造函数。因为array没法初始化对象(使用{}初始化是可以的)。

4、array可以使用拷贝(容器类型,元素类型,大小都要相同),内置类型的数组不行。不过在拷贝的时候,只能使用对象,用{}就不行(不行指的是arr={}这样不行),应该是因为拷贝构造是explicit的。

list | forward_list

1、forword_list只允许push_front,因为如果要从后面放会很慢。再设计上为了简单,则只能在前面插入。

2、对于list,无法使用全局的sort函数,因为全局sort的参数的迭代器必须是随机访问迭代器,即可以+,-,*,/,+=,-=等。但是list的迭代器是指向节点的,无法完成上述操作。

3、对list源码的解析:

整个list里面只有一个数据,是一根指针,指向一个由data、next、pre形成的对象,其实是一个节点的数据结构。所以链表靠一个指向节点的指针就可以控制整个数据结构。

list整个实作表现出来的是环状的设计,由于需要一个end迭代器指向最后一个元素的下一个元素,所以设计了一个虚拟节点来实现环状以及end迭代器。

4、对于单向链表,我们知道单向链表无法访问其前面的元素,那么为了能插入删除,就定义了insert_after,emplace_after,erase_after操作。以及一个首前迭代器(调用before_begin函数),用于链表在首部插入的操作。

vector

1、vector靠三根指针来控制整个容器,分别是指向第一个元素的指针,指向最后一个元素下一个位置的指针,指向申请空间末尾的下一个位置的指针。而且vector是会增长的,每次两倍增长。vector的迭代器是普通的指针,因为其存储空间是连续的,所以不需要特地设计一个特殊的迭代器。

2、vector对象也能比大小,不过一般都不用这种操作。==表示元素个数,类型,值都相同,<要不就是其他都相同,但小的那个个数少,要不就是以第一个不相等的位置的元素的大小关系来衡量。>号和小于号类似。

deque

1、接下来是deque。deque在外部表现为连续的,在实现上是分段连续的。有一个vector来存储指向一系列buffer的指针,这些buffer是连续的。先说说deque的成员变量:有两个迭代器成员,分别为start与finish,表明当前已有数据的头与尾,每个迭代器是四根指针,分别对应当前buffer的头,尾,当前指向的值与指向的vector中的位置。然后就是一个指向指针的指针,其实用来指向那个vector的。最后一个是map_size,表明指针vector的长度(包含未用的那些)。deque的一个buffer大小是512字节/元素占用的字节数。

queue | stack

1、stack和queue,不提供迭代器,不然破坏了它的数据结构。

2、deque与stack可以看成容器的适配器,底层默认是用deque实现的。

set | map

1、对于unordered_set与unordered_map等的元素,我们内部是将元素通过哈希函数转换为哈希码,然后进行存储。存储的方式为由一系列的bucket,bucket有一定个数,将元素转换后的哈希码%bucket个数,最后得到的余数就是对应元素应该放到哈希表的bucket的位置,一系列bucket是用vector实现的,其中放的元素是指向链表的指针。如果不同元素算得的哈希码对应到同一个地方,则将元素链接于链表之后。bucket的数量会成长,经验表明当元素个数大于bucket数量时,就该成长。这时候所有元素都得重新算一回哈希码。成长比较耗时。

2、multimap不像map一样能够用[]来插入元素,因为有多个相同的key可以对应。那类推unorder_multimap也无法使用[]来插入元素。

3、关于map,set底层的红黑树,目前还是很难去了解,所以只是稍微讲下,其底层是红黑树实现的。模板参数Key是我们熟知的key的类型,Value是pair<int,int>这样的组合,KeyofValue指如何从那个组合中取出Key的值,Compare指Key类型的对象如何比较大小。

红黑树有两个插入方法,insert_unique、insert_equal分别对应不可重复插入以及可重复插入。

4、set内部包含了一颗红黑树,它的数据就是红黑树。但是它的迭代器和我们之前见到的list的不一样,set的迭代器借用了红黑树的const_iterator,这个迭代器不允许修改元素的内容。而set各种操作都交给底层的红黑树去做,所以set也可以说是一个容器适配器。

5、map内包含一个红黑树,它的数据就是红黑树,它的迭代器与set的不同,使用的是红黑树的iteartor,因为虽然无法修改key的值,但是可以修改value的值。map也可以说是一种容器适配器。

6、关于哈希表的一些概念,其实前面已经讲过了,主要关注哈希函数以及哈希表的成长方式。哈希表的数据结构中,有三个仿函数对象,除了这三个,最重要的就是一个vector,这个vector中的元素相当于一系列的bucket,指向一条链表。还有一个变量,记录当前插入的元素数。关于哈希表的迭代器,是由一根指向当前某条链表中一个元素的指针以及一根指向某个bucket的指针构成。这样就可以达到外部连续的结果。

关于模板参数:Value类似pair<int,int>这样的类型,Key用于计算哈希码的那个类型,HashFcn哈希函数,ExtractKey如何从Value里面提取Key,一般有一个selectfirst类作为这个模板参数,EqualKey Key怎么比大小。

7、关于unorder_set,unorder_multiset,unorder_map,unorder_multimap,其底部也是包含了一个哈希表,然后迭代器用的是hash_table的迭代器,也可以算是一种容器适配器。

8、万用的hash_func,还是备份一下,以免忘的干干净净

我们可以写出标准库中hash的重载版本,但是记得在namespace std中写。

我在写程序的时候发现,如果要给unordered系列的容器仿函数,最好定义为struct,如果定义为class一定要加public!我在这坑里搞了好久。

9、对于set定义自己的排序规则:

10、关联容器新的typedef:

key_type: 键的类型

mapped_type 只适用于map,值类型

value_type 对应于set就是key_type,对应于map就是pair<const key_type,mapped_type>

使用const时因为我们不能改变key的值。

11、set中的迭代器不管是set<>::iterator还是set<>::const_iterator都是只读的。

12、对于不能包含重复键的容器,使用insert和emplace会返回一个pair,这个pair返回一个指向与插入键相同值的元素的迭代器和一个bool,表征是否插入成功。

而如果允许重复的键,只会返回指向新插入元素的迭代器。

13、map<string,int>M;

M[“aa”]=1;先插入键值对(“aa“,0),然后再赋值1,所以经理了初始化与赋值两种操作。下标操作只能用于非const版本的map,unordered_map。

14、使用unordered容器的时候,需要一个哈希参数以及如何表征元素相等的参数。

tuple

1、tuple一个简单的用法:

2、tuple之间可以直接赋值,也可以比大小,需要成员数量相等且类型可转换才行。get<i>使用的时候要求i是个整型常量表达式,因为i是非类型模板参数的实参,这个实参要求就是整型常量表达式。

3、tuple的一些用法:

4、tuple的相等或不等的运算符必须要求tuple的成员数量是相同的,而且每个对应的成员之间的关系运算符都是合法的。

iterator

1、迭代器有5种:输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机访问迭代器。

2、对于迭代器,首先清楚它是类,但是这个类实现在*,->等操作符,完成了智能指针的功能。迭代器类里面定义5个重要的typedef。(所有的容器的迭代器都得有):

value_type 表明容器操作的元素类型

pointer    容器操作元素的指针类型(没被使用过)

reference  容器操作的元素的引用类型(没被使用过)

iterator_category  迭代器分类(5种迭代器之一),即有些迭代器只能++,有些可以再--,也有可以+=,-=

difference_type  两个迭代器的距离应该用什么类型来表现。

这些属性,有一个iteartor_traits可以来获得,不过按理说我们自己直接用容器的迭代器::属性就可以获得相应的属性,不需要traits。实际出现这个的原因,是当使用标准库的算法时,如果传入的是个普通的指针(普通指针也是一种迭代器),就不存在这些属性,iterator_traits就是用来处理普通的指针的。使用方法如下,相当于加了个中间层:

3、之前说过标准库容器的迭代器里面必然有5个typedef。而iterator_category表明容器的一些特性,现在展示具体的特性值:其实是一系列类。

input_iterator_tag与output_iterator_tag比较特殊。这边以继承的方式获得描述迭代器类型,其实是有深意的。因为在标准库的算法中,一些算法只实现了小部分迭代器类型的区分,如果传入一个未被实现的类型,可以通过继承关系,往父类方向寻找那个实现版本。

4、rbegin()和rend()表示翻转的开头与结尾。如果有一个vector内的元素是{1,2,3},rbegin指针位置处于最后一个元素的下一个元素处,但是它在取值的时候,会取它前一个位置的元素,即3,rend处于第一个元素的位置,但是取值的时候会取第一个元素的前面那个位置的值,所以rend不能用来修改值。它的那种取值方式是由于被一个叫做reverse_iterator的迭代器类给包装了,即适配了一下,reverse_iterator内含了一个正常的迭代器,但是将迭代器向后取值的行为修改成了向前取值,实际一些操作都靠内部的正常迭代器在操作。

顺便介绍下cbegin与cend,c表示const,所以应该是返回的迭代器无法修改吧。

5、有个迭代器适配器inserter,超级巧妙。如果要将一个容器M的数值插入到另一个区域N,使用copy(M.begin(),M.end(),N.begin());这样做是将N从begin()开始覆盖。而实际我们想要的往往是插入,而不会覆盖。

首先明确,copy中的行为已经写死了,那如何改变copy的行为呢。答案是将N.begin()以及一些需要的部分用inserter包装,那么传入copy的最后一个参数就是inserter_iterator类型,而这个类型去重载操作符,因为copy中对第三参数无非是操作符的应用,而inserter_iterator重载操作符可以改变未包装前的迭代器的行为。同理,ostream_iterator也是用相同的做法做出来的,它能够绑定cout,使得copy操作实际上在输出。istream_iterator绑定cin,然后重载*操作符表示返回当前值,重载++操作符表示从键盘读取。注意,在istream_iterator初始化的时候,已经开始了第一次读取,所以这个在使用的时候可能会带来困扰。

6、和insert类似,有三个:inserter、front_inserter、back_inserter。front_inserter、back_inserter接受一个容器,然后只有一种操作,就是=操作符。而inserter接受一个容器与位置,表示插入的容器以及具体的地址,只有一种操作,就是=操作符。这三个适配器返回对应的插入迭代器。对这些迭代器做=就是做对应的插入动作。

7、容器的iterator可以改变容器内的值,const_iterator不行,所以当存的是常量或容器对象是常量,就只能用const_iterator。同时,当对象被const修饰,则begin和end返回const_iterator类型的对象。

8、两个迭代器可以相减,表示的是俩迭代器之间的距离。

9、获取数组的头尾迭代器使用begin和end函数,但是其返回类型是指针,而不是迭代器类型。

10、反向迭代器

反向迭代器的对象有一个base函数,可以返回正常方向的迭代器。

11、make_move_iterator函数返回移动迭代器。将一个普通迭代器的解引用操作改写,返回一个右值引用。

algorithm

1、标准库的算法使用的是模板方法实现的,而不是利用仿函数实现的。

2、for_each是算法的一种,传入两个迭代器和一个对象(仿函数或函数指针),使得在迭代器之间的元素都做传入的函数的操作。

3、标准库的很多仿函数类似greater,less都继承了一个类binary_function,表示含有两个操作数,还有另一部分函数继承unary_function,表示一个操作数。这两个类很简单,只是一些typedef

这样做的用意在意,当仿函数被适配器改变的时候,适配器可能会问仿函数一些问题,而此时需要这些typedef来回答,或者算法也会问仿函数这几个问题。所以如果想要写出能够融入标准库的仿函数,需要继承这两个类的其中一个。

模板

一般规则

1、特化的写法,必须有一个泛化的模板作为前提:

2、偏特化有两种,个数上的偏特化与范围上的偏特化,偏特化前必须有一个泛化的版本。而且参数的个数总是跟着泛化版本来。个数上的偏特化,其实特化可以看成模板参数为一个的数量上的偏特化,而对于两个或两个以上的偏特化,按照下面方式:

3、范围上的偏特化非常有趣可以对输入模板参数的范围进行限定,范围特化支持模板的高级用法。代码示例如下:

4、模板模板参数,有一个非常值得讲的地方,具体看下图:

5、理解可变模板参数…位置:如果要知道一包有几个,使用sizeof…(args)可获得。注意:Args…可以是0个。

6、对于可变模板参数,以下两种情况可以并存,且有两个模板参数的更加特化一点:

7、化名模板,形式如下:无法对化名模板特化或偏特化。类型化名:形如using A=int; 和typedef是一样的。

8、对于特化,是声明类型就对应声明版本,而不会隐式转换。比如一个类A可以转换为double类型,而一个函数模板对double有特化,但对A没有特化,那在函数里用A的对象是调用泛化版本,不会出现隐式转换。

9、一个模板用的小技巧,我们一般在使用类模板的时候,一定要用<>给定真正的类型。而函数模板可以做实参推导,那我们就可以使用函数模板的参数来作为使用类模板的中间层,那样在使用的时候直接用函数就好,不需要指定类型,函数模板会自动做实参推导。

10、使用模板的特化可以实现一些amazing的功能,比如下面移除const特性:

11、对于模板的非类型参数的用法,一定要放常量表达式(因为需要在编译时绑定):

非类型参数可以指定为整型、指向对象的指针或引用。绑定到指针或引用的的非类型实参必须具有静态生存周期(静态变量)。

12、通常,当我么调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须可用,成员函数定义可以不必出现。而模板为了生成一个实例化版本,必须要有函数模板完整定义,或类模板成员函数的定义。(不知道什么意思,如果是是哟个之前一定要出现定义,我的编译器不是这样的)

之前提到过友元的使用一定要定义后才行。

13、对于sizeof…,返回一个常量表达式,然后其也不会真正计算传入的值。可用于可变模板参数与可变函数参数。

一般使用可变参数模板方式为递归,所以需要一个非可变参数模板或非模板函数来终止递归,非可变参数模板比可变参数模板更加特化。

14、可变函数模板的组成为模式与扩展。

模式可以比较复杂,甚至可以是函数调用,扩展的…放在最后面:print(os,func(rest)…);…放在哪决定了拓展什么。

可变模板参数的转发和普通模板参数的转发一样,使用forward。关键在于扩展的使用。

15、

特化是一个实例,而不是一个重载版本,因此特化不影响函数匹配(模板之间的匹配)。但是非模板还是比模板要更加特化。

泛化要在特化之前定义,使用特化时特化的定义也要在使用之前出现。

16、类模板特化可以只特化一个成员:

类模板 | 函数模板

1、成员模板,其实是外部有个模板,然后在类的内部又定义了一个模板,有什么用呢,我个人觉得是使得内部知道传入数据的类型吧,就算传入的数据被转型了,也知道转型前是什么类型。下面的图片中,构造函数的a是浮点类型,而val是整型。

2、一个比较实用有有技术含量的类模板:type_traits用于获取类的一些特性。type_traits是一个模板版,表明输入的模板类型的一些属性,比如默认构造、拷贝构造、拷贝赋值、析构函数等是否重要。

上边只是早期版本,后面又很多更加牛逼的type_traits,比如获得输入是不是const,有没有虚析构函数等。

3、对于类模板在类外定义的成员函数,需要这么来(定义在类内的成员函数默认inline):

4、类模板的成员函数只有在使用到的时候才实例化。所以即使实参类型可能完全不能匹配当前操作,也能定义那个类型的模板的对象。

5、在类模板使用时一般需要提供模板实参,只有在自身类模板内使用时可以不加模板实参。对于在类外定义的成员函数,返回类型不在类作用域内,需要模板实参,而函数体是在作用域内的,则不需要加模板实参。

6、当一个类模板中包含一个友元声明时,类与友元各自是否是模板是无关的。如果一个类包含一个非模板友元,则友元可以访问所有模板实例;若友元是模板,类可以授权给所有的友元模板实例,也可以只授权给特定实例。

还有其他情况的友元,总结来说,当友元是个模板的时候,指定特定模板参数则这个特定模板参数是友元(这时候这个模板类一定要前置声明下),指定为与当前类一样的模板参数,则一一对应(需要前置声明)。指定为另一个模板参数,则任意的实例都是当前类的友元(不需要前置声明)。

7、把自己模板参数的类型当成友元。

8、typedef无法定义一个模板,但是可以使用类型别名定义模板。

9、对于类模板的静态成员,每个具体的模板参数的类型共享静态成员,不同的模板参数的静态成员不共享。当静态成员在类外初始化的时候也要模板:

10、与模板类的其他成员函数一样,一个模板类的static成员函数需要在使用时才初始化。

11、模板参数会隐藏外层作用域中声明的相同名字。而且模板内不能重新定义模板参数名。

12、模板声明与定义中的模板参数不需要相同,但是种类与数量必须一致。

13、为了表征某些成员是类型,使用typename T::a。这时候只能使用typename而不能用class。

14、对于模板参数,可以有默认实参,如果所有模板参数都有默认实参,类模板在使用的时候还是需要加<>。

15、普通类与类模板可以有成员模板,但是不能是虚函数。

16、对于类模板的成员模板,如果在类外定义成员函数时,要同时写出类模板与成员模板。

17、在多个源文件中使用了相同的模板实例化,每个文件就有该模板的一个实例。

为了减少这种浪费,使用实例化声明:

extern template class A<int>;

表示在其他文件中实例化了A<int>。

在文件中写template class A<int>则表示实例化定义了这个模板参数的类以及其成员。否则只会用一个成员实例化一个。当然用某个类型来实例化定义的时候,这个类型要符合这个类的所有成员函数。比如假设定义了一个<操作符,那输入一个无法用<比较的类型就不行。

18、模板类型参数的实参会进行类型转换,但是与一般的转换不同,只有很少部分能用上(前提是类型是推导出来的):

顶层const,能够将非const对象的引用(或指针)实参。传递给一个const引用或指针形参。

如果函数形参不是引用类型,则数组转换为指针,函数实参转换为函数指针。

其他比如整型提升、派生类向基类的转换都不行。

而那些没有定义为模板参数类型的变量类型其转换规则和普通的规则一样。

当我们显式指明了类型,就可以像普通的类型转换那样工作。

19、对于函数模板,有时候我们无法推断模板实参的类型(比如函数返回类型,当返回类型和形参都不一样时,),则需要显式指明。而且可以指明部分,隐式推导部分。不过隐式推导部分只能放在模板参数列表的最后。<typename T1,typename T2> --> <int>  T1为int,T2自己推导。

20、我们可以通过函数指针获得实参的推导:

21、作为函数模板推导,当模版类型应用的时候加了const的时候,推导得到的结果不能是const。

22、对于一个右值引用的模板,我们可以给左值,这时候一个特殊规则是左值被解析为引用:这样参数类型就变成引用的引用。我们一般不能定义引用的引用,在模板或类型别名中是可以的。

根据折叠规则:X& &,X& &&,X&& &最终折叠成左值引用X&

X&& &&折叠成右值引用X&&。

23、move的源代码是使用强制转换完成的 。

这主要是static_cast的特殊规则,即可以将左值引用强制转换为右值引用。

24、当将一个模板参数定义为右值引用类型,可以保持传入类型的引用特性以及const特性(得在模板中,因为非模板情况下右值引用的形参都不能使用左值的实参)。

25、函数参数永远都是左值,即使传入类型是右值引用。所以这会导致一个问题,如果在形参为右值引用的函数中再次使用右值引用,就永远不可能成功。

这时候需要forward函数,使用为forward<T>(args);返回T&&。所以对于T是左值引用的情况,最后会折叠成左值,而T是右值引用的情况或一般类型,最终都是右值引用。

26、对于函数模板的重载:如果多个函数都能匹配调用,首选非模板函数,其次选相对特化的那个,再没有就是歧义。同样的。模板类型转换只有顶层const以及函数与数组转换为指针。

27、注意只有类能够偏特化,函数不行,不过函数可以特化。其原因是已经有模板函数的重载了,不需要偏特化。(特化func后的<int>可以不写,因为实参推导。但是有些不能忽略的就得写上)

内存管理

1、内存管理总图概览

2、debug模式的malloc与release模式的malloc如下:对于array new,array delete下创建的数组,在真实对象的参数上还有一个对象的数量,占4字节。

3、关于在栈中的变量以及在堆中的变量:栈中变量离开作用域自动销毁,而堆中的变量离开作用域还是不会销毁,需要使用delete手动销毁。需要注意的是,在作用域内创建堆的变量,一定要将指针传出去,否则造成内存泄漏。

4、关于operator new,operator delete,operator new[],operator delete[],我们可以重载,放在类外是全局的,放在类内是局部的。对于operator new/new[],第一参数一定是size_t的,而且返回一定是void*。这个函数由操作系统调用。operator delete/delete[],输入指针,返回为void。当使用new的时候,如果对象是类,会先从类的里面找operator new,找不到再到类外找全局的。

operator delete(void*,size_t) operator delete(void*)在类成员里面只能出现其中一个,不然无法知晓到底调用哪个。同理operator delete[]也是。如果只存在operator delete(void*,size_t)也并不会调用这个。

5、使用new 创建对象的时候,会跳转到类内部定义的operator new/new[],如果要绕过自定义的operator new/new[],可以使用::new绕过。delete同样如此。还有一个placement new,其实是对operator new的重载,但是参数不止一个,但是第一个一定要是size_t。使用如下:A *p=new(10) A;表示有一个第二参数是整型的。

6、malloc与operator new都返回void*,而allocator会返回一个具体类型的指针,因为它有模板参数被告知需要的是什么指针。GNU2.9的内存池是按照字节分配的,所以返回的也是void*。

7、关于new表达式,可以简单转化其表达式做的事:

我们无法通过外部指针调用类的构造函数,只有编译器有这个权限。//分配内存->类型转换->构造对象以及返回指针

对应delete表达式的转化:我们可以通过对象的指针调用析构函数,这与构造函数不同。//调用析构->释放内存

8、对于placement new,前面讲过它是重载了operator new,多了几个参数而已,其最经典的用法是,传入一个地址,也就是一根指针,然后placement new会调用全局的operator new(创建对象的过程是这个时候进行的),其第二参数是指针,然后这个函数不会去底部调用malloc,而是直接返回传入的指针,所以我们其实是在已经分配好地址的地方,创建了对象。实际上void* operator new(size_t,void*);这个最标准的placement new是不能被重新定义的。(我的编译器可以哎。。)。注意,使用placement new创建了对象之后需要显式使用delete析构。直接delete会有问题,会导致原变量在释放的时候出毛病。

9、在类内重新定义operator new/new[]或者operator delete/delete[],一定要写成静态的。因为有可能在还没对象前,就需要调用这些函数。不过c++因为知道这些必须为静态,所以会自动添加,我们写不写无所谓。new->operator new    new[]->operator new[]。

10、一个简单的内存管理,这边有一个令人难以理解的地方,为什么没有对应的free不算内存泄漏呢,当程序执行完,那块分配的内存区应该也是在的啊。解释为:当主程序这个进程结束时,没有delete的内存还是会回收,但是这个回收是操作系统内存管理的范畴,由计算机来回收,而不是由程序来回收。

11、new handler是在使用new分配内存而内存不足时出现的情况,用法如下:返回void,且没参数。

12、对于GNU2.9经典的内存池的设计,即pool_alloc,我们可以发现这个分配器是全局的,即所有的对象都通过它去分配内存,当战备池和链表处都为空的时候,每次分配会获得20个所要求大小的区块和20个所要求大小的战备池和一个追加量(追加量=累积申请量/16)。每次需要存储的时候,先去战备池要,至少能拿一个,拿不到就把战备池挂到其大小相同的链表处(碎片的处理),然后重新用malloc申请。其实申请来的内存也是先放到战备池,然后再从战备池分部分到链表而已。当系统内存山穷水尽的时候,即无法通过系统获得内存时,就从向右相邻的已分配的内存区获得一块拿来切割出需要的大小以及剩余作为战备池。

内存池里面有一个包含16根指针的数组,每根指针指向一个链表,表示不同内存大小下的小内存池设计。内存池的数据:一个数组,存的是16个指针,两个指针指向备战池,以及一个变量已分配内存。

13、c++在进入main之前与之后的一些程序:

heap_init先申请一块内存,然后用申请的内存创建16个header,每个header最终会管理1M的内存。header数据结构通过调用windows的virtual_allocator获得内存(只有使用的时候才会真正分配内存)。header的数据结构如下:

然后执行ioinit,其中第一次使用malloc。malloc分为debug模式和release模式。以下是debug模式下的debug头数据结构:

到这一步就是把debug的头需要的内存与原申请数据所需的内存大小相加,获得带debug大小的内存总量。再接下去就是加上上下cookie以及调整到16的倍数,这样内存总量就是我们熟悉的cookie间的总量(包含cookie)。前两个指针应该是最终空闲内存串起来用的指针,szFileName表示申请内存的文件名,nLine表示的是文件的第几行,nDataSize表示真实数据的大小(即去掉debug头等信息),nBlockUse分c_runtime与main,是两个宏。

然后就要开始真正分配内存,前面都是在计算分配大小而已。首先header申请1M的内存,为了管理这1M的内存,就新建一个REGION的数据结构来管理,REGION需要的地址空间是从一开始申请的4096大小的内存拿。BITVEC是unsigned int。

关于数据的含义:sizeFront应该代表的是当前指向的地址空间空闲块的大小。

bitGroupHi和bitGroupLo的每一个对应的元素是拼在一起的,拼在一起就是8个字节,64个位,表示group内64条链表有没有挂内存,1表示有内存。那为什么有32个呢,因为有32个group。cntEntries表示当前的被分配了几次,用于全部计数回归0的时候还给操作系统。indGroupUse是当前在使用的Group编号。

如上所述,一个header有32个group,也就是把1M分成32块来管理每块32K,然后这32K又是分为8小块,每小块叫做一个页框的大小。这些小块被串起来,然后挂到group数据结构中对链表的最后一对。其实这边会有疑惑,为什么要这么多对指针?答案是每一对管理不同大小的内存,第一对16个字节。第二对32个字节…和内存池的设计类似。最后一条为大于1K的都归它管。

当内存分配的时候,先找最合适的那对指针上有没有挂内存,如果没有就向后找到适合的最小内存,然后分配,剩余的部分挂到适合的位置。对于内存回收,就是直接挂到合适的位置即可。当然由计数,标志位以及cookies等都得改动。

对于cookies,需要上下两块,下面那块主要用在内存回收的时候定位长度。

当某个Group分配出去的计数返回0的时候,就是全回收。对于全回收的内存,会有延缓的操作,也就是没有其他的全回收的时候,这个内存不会被回收,而是继续保持着。如果有其他的全回收,则释放其他全回收,将当前全回收给留下。

14、栈中分配局部变量空间,堆区是向上增长的用于分配程序员申请的内存空间。另外还有静态区是分配静态变量,全局变量空间的;只读区是分配常量和程序代码空间的;以及其他一些分区。

程序的内存分配:
一个由C/C++编译的程序占用的内存分为以下几个部分:
栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(静态变量和全局变量会默认初始化,所以不明白为什么会分开)。程序结束后由系统释放。
文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放。
程序代码区—存放函数体的二进制代码。

15、Loki的结构:

loki的结构其实很简单,Chunk维护一个请求的缓存区,分别是这块内存的首地址,当前可用的块序号(这个就表明每块大小是固定的,由上层传进来),以及剩余可用块。需要注意这边内存块利用第一个字节来表明下一个可用块在哪。回收的时候是计算出回收块所指定块序号,并放在当前空闲块的最前面。

FixedAlloc里面是Chunk的vector而且以及两根指针,vector肯定是为了防止一个Chunk不够用的情况。allocChunk指向上一次分配的区块,也就是为了上回在这一块分配,这回我再从这一块开始找,为了效率而已。还有一根是deallocChunk,为了回收的效率而设计的。这边需要注意的是vector成长的时候会出现地址变化,所以由使得vector长度变化操作的时要修改之前指向vector元素的指针。

而最后的SmallObjAllocator不需要提,因为只是简单的管理不同申请空间大小,就像pool_alloc的16条链表那样。

16、最后一个重要的alloc,bitmap_alloc:

前面的这么多F叫bitmap,表示后面的存储区是否被用掉。而后面的一格存储区叫做一个block,最前面的0表示可用区块数。这三个区域加起来叫一个super_block。在super_block前面还有4个字节保存后面那么长占多少字节。

bitmap_alloc里面有一个vector维护。vector的元素就是指向一个super_block的存储区。对于super_block的规则,是这样的,每次增长放的block数量是前一次的两倍。不过只要有一块全回收,下一次分配就减半。关于回收,会有另一个vector,根据回收区的大小,从小到大管理回收区。到回收区大于64个的时候,就真正释放最大的那个。当需要分配又没有内存时,会先从回收区拿。

16、动态分配内存的对象时默认初始化的,所以一般的类型时未定义的,而类就看构造函数怎么写了。但是加上()就可以值初始化了。

17、delete只能释放动态分配的内存以及空指针。不能多次释放相同的指针。

18、array new即new[]在使用时,[]中的值可以是变量,不必是常量。注意:使用new[]返回的不是数组类型,而是个指针,所以无法对它调用begin与end函数。也不能用范围for循环处理new[]中的元素。

默认的new[]中的元素是未初始化的,使用如下方法初始化:

由于new[]()的括号中不能有值,所以不能像new一样使用初始化器自动推导类型:

new[]的一个与数组不同的操作:

19、delete[]

delete[]删除元素是从最后一个元素开始的逆序销毁。

即使使用类型别名把数组类型看成了一个整体,也要使用delete[]来销毁。这与const修饰的类型别名不一样:

20、对allocator的拓展:

众所周知,allocator的allocate和deallocate分别申请内存和释放内存,其分配的内存是未构造的。而allocator有两个神奇的函数construct:输入要构造的地址以及不同数量的参数,只要那些参数符合所放元素的构造函数即可;destroy:输入要销毁的地址即可,销毁之后就可以重新构造(此函数存在的意义是为了调用元素的析构函数)。

21、关于operator new/operator delete:当外部调用new/delete时,如果被分配的是类类型时,编译器首先在类及其基类的作用域中查找opreator new/operator delete。否则就在全局查找。所以要重载全局的就写在全局作用域,重载类的就写在类里面,这时候重载版本是隐式静态的。::new, ::delete表示在全局作用域查找。

22、operator::delete不允许抛出异常,当重载的时候我们也要加上noexcept。

我们可以重载operator new与operator delete。但是以下形式不能被重载:

void* operator new(size_t,void*);  也就是标准的placement new。

在类内定义的operator delete/operator delete[]能有第二个size_t的参数,代表待删除的字节数,如果待删除的指针指向的析构函数是虚函数,则传递给operator delete的字节数也会随之变化。由动态类型决定。

23、程序可以直接使用operator new/delete operator new[]/delete[] 而不需要使用new/delete间接调用。这时候,分配的内存空间是为初始化的。所以需要placement new来初始化空间:

new(p) type

new(p) type()

new(p) type[]

new(p) type[]{}

placement new可以不指向动态内存。它简单的返回传入的指针,然后用new表达式初始化对象。

24、调用析构函数会销毁对象,但是不会释放内存。我一直以为析构函数是释放指出去的内存,但实际也会销毁自身的对象。而真正释放内存要到operator delete。

IO流

1、宽字符版本的IO库对象有wcin,wcout,wcerr等。我们对于多种IO类型之间用法是一样的,这是利用继承机制实现的。

2、endl输出换行再刷新缓冲区,ends输出空字符再刷新缓冲区,flush什么都不输出再刷新缓冲区。还有一个unitbuf用法:

cin与cout关联,所有想要读取的数据在我们手动输入时会出现在控制台的打印上。

对于流的关联,可以使用tie函数,一个tie无参数,返回当前绑定的输出流的指针,一个有参数,为绑定一个输出流。可以将istream关联到另一个ostream,也可以将ostream关联到另一个ostream。每个流最多关联一个流,但是多个流可以关联到同一个流。

cin.tie(&cout);

3、文件流:ifstream(读).ofstream(写),fstream(读写)

这些操作和cin,cout很像,只是缓冲区变成了文件。为了关联一个文件,必须首先关闭打开的文件。当fstream对象被销毁,close自动调用。

文件模式:

in  读方式打开

out 写方式打开

app 每次写都定位到文件末尾

ate 一打开就定位到文件末尾 

trunc 文件内容会被覆盖

binary 二进制形式进行IO

fstream可以设定读写方式,ifstream只能读 ofstream只能写。

4、string流

其实就是把string给当成了缓冲区,然后去操作string;不会改变原string,拷贝进去的string也不会被改变。与cin一样,读取字符或字符串会忽略空格回车等。与cout一样,输出其实是把数据写入到string流,这就和文件流类似,会覆盖。

5、iostream迭代器

作为iostream类型的迭代器

使用++为读取一个元素,*为获得读取的值。

标准库保证在解引用一个流迭代器之前,从流中读取的操作已经完成。不给istream_iterator绑定流就是定义尾后迭代器。

istream_iterator还有尾后迭代器,而ostream_iterator没有尾后迭代器,其必须绑定到一个流。ostream_iterator有第二个可选参数,必须是char*的字符串,不能是string。表示每次输出之后就会随之加一个此字符串。流迭代器不支持--。

6、格式化输入输出:

在scientific、fixed或hexfloat之后,精度控制是小数点后面的数字位数。

我们可以使用未格式化输入输出读取流,把流当成一个字符序列:

cin.get(ch)      cout.put(ch)

这样取得的值不能忽略空白格。我们在使用get的时候,千万不要把读取的ch设定为char。因为返回的是int。转型之后可能导致EOF永远不会出现。

369.对于流的随机访问,使用seekg/seekp以及tellg/tellp函数来设置/获得输入输出流 的标记点。由于istream与ostream不支持随机访问,所以一般用于fstream与sstream。

g代表get即输入流,p代表put即输出流。

对于同一个流来说,你的输入与输出的标记点是同一个。比如你先读取文件的部分数据,然后开始写入,写入点就是你刚刚读的数据的后面。

其他

异常检测

1、异常被throw之后,沿着调用链一层层向外查找直到找到符合的异常catch块。这个过程称为栈展开过程。如果找到,执行catch的代码并找到与try块关联的最后一个catch子句之后的点,并从这里开始继续执行。(没懂)。如果没找到,则调用terminate终止程序运行。

2、我们可以用throw抛出各种异常,而被抛出的叫异常对象,catch捕捉异常对象之后对异常对象进行拷贝初始化:所以看上述代码,我们其实可以自定义异常对象。异常对象需要满足有可访问的析构函数,和可访问的拷贝或移动构造函数。数组和函数的一场表达式都被转型为指针类型。

3、在栈展开过程中,我们退出一个块就销毁其中的局部变量,所以抛出一个指向局部变量的指针几乎肯定是一种错误的行为。当抛出表达式时,看的是静态编译时类型,而不是动态类型。(与多态有关的一个东西)。

4、catch子句的异常声明的声明方式决定了代码所能捕获的异常类型。可以声明为左值引用,但是不能为右值引用。

可以用基类来捕获子类的异常对象,如果使用非引用形式,则异常对象会被切除一小部分。

异常声明的静态类型决定了catch语句所能执行的操作。如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。

5、在搜寻catch语句中,不是看哪个最匹配,而是看哪个先被找到。所以越是专门的catch就越应该放前面。

6、catch异常声明匹配也要求也很高。相对模板匹配类型转换,catch多了一个:

顶层const从非const到const

派生类转换为基类

数组和函数转换为指针。

7、我们可以重新抛出异常,只需要一句空throw语句,但是空throw语句只能出现在catch或catch直接或间接调用的函数中。异常对象是当前的异常对象。除非当前的异常声明是引用类型,否则重新抛出的异常在接下来处理的时候还是与throw那会的对象一样。

8、我们可以使用…作为异常声明,它捕获所有异常。catch(…)与别的异常声明一起出现时一定要放在最后,不然别的异常声明相当于摆设(一位内异常声明是按顺序查找而不是看最匹配)。

9、有一个函数try语句块或者叫函数测试块,处理构造函数的初始化列表或函数体中发生的异常,同时也能处理析构函数

10、对于noexcept要么出现在函数的所有声明和定义语句中,要么一次都不出现。noexcept不能放在typedef或类型别名中,noexcept放置的时候要在const &限定符之后,在final override =0之前。

11、编译器在编译的时候不会检查noexcept,所以就算在noexcept的函数中加了throw语句也不会报错。但是运行时会因为冲突而终止程序。

12、noexept有两种,一种叫异常说明符,放在函数后面的,一种是异常操作符,是个一元表达式,返回一个bool类型的右值常量表达式。

noexcept说明符可以包含参数,很早就说过了。用法为:

noexcept(e)如果e所有调用的函数声明为noexcept(true)而e本身没有throw,则,noexcept(e)为true,否则为false。

noexcept说明符与操作符混用:

void f()noexcept(noexcept(g()))  //f和g的异常说明一致。

noexcept不属于函数类型的一部分。

13、函数指针可以设定异常说明符。当指定为不抛出异常,只能指向不抛出异常的函数,否则就可以指向任意函数。

void (*f)(int,int) noexcept = func;

14、如果基类的虚函数设定为noexcept,则子类的相同虚函数也需要设置noexcept。相反,基类没设置,子类可以自由设置是否noexcept。

15、对于编译器合成的拷贝控制成员,也会合成一个异常说明。当对所有成员和基类的所有操作都是不抛出异常的,则noexcept(true),否则就是noexcept(false)。析构函数也生成异常说明,默认noexcept。

智能指针

1、三种智能指针:

shared_ptr、unique_ptr、weak_ptr。

shared_ptr允许多个指针指向同一个对象。

unique_ptr独占所指向的对象。

weak_ptr是一种弱引用,指向shared_ptr所管理的对象。

shared_ptr:

其内部有计数器表征其保存的对象被其他对象引用的次数。还有一个get成员函数获得其所指对象的地址。当智能指针的引用数位0时,就会销毁所管理的对象。

使用make_shared函数获取一个shared_ptr类型的对象,其必须绑定一个类型,函数的形参和给定类型的构造函数的参数必须对应。

可以用一个new返回的指针来初始化shared_ptr,但是只能直接初始化,因为其构造函数是explicit的。出于相同原因,函数返回shared_ptr类型也无法通过隐式转换得到:

使用reset绑定新的对象:

shared_ptr可以绑定第二参数,是个函数指针,当作delete的替代。

2、在整理shared_ptr的计数器的时候,发现了一个有趣的问题,其原因是编译器的优化,使得构造与析构少了几次。

3、在函数中,不管正常退出还是发生异常,局部变量都会被销毁。所以使用智能指针是安全的。而如果使用new delete,在delete之前发生异常的话,new的内存不会被销毁。

4、unique_ptr

unique_ptr只能使用直接初始化,不支持拷贝与赋值(但是可以拷贝和赋值一个将要被销毁的unique_ptr),而且要绑定到一个new返回的对象指针上。

给unique自定义删除器即delete功能的函数的时候,需要指明调用的删除器的类型。

5、weak_ptr

weak_ptr指向shared_ptr所指向的对象,weak_ptr绑定shared_ptr时,不会影响shared_ptr的计数,而如果shared_ptr指向的对象销毁时,weak_ptr也不能用了。所以在使用weak_ptr的时候,不能直接使用,而是需要使用其lock函数,如果对象存在,返回一个指向对象的shared_ptr。

6、unique_ptr提供一个管理new[]的版本。在访问元素的时候使用下标访问。

而shared_ptr没有特定的版本。所以需要提供自己写的删除器才能使用:

7、关于shared_ptr计数器的实现,可以把计数器也用动态内存分配,对象拷贝的时候把指向动态内存的指针拷贝了,就可以操纵同一个内存了。而不是用什么静态变量奥。

大杂烩

1、 防卫式申明方式防止重复包含头文件。

2、assert

assert(expr);

expr为假,则输出信息并终止程序,expr为真,则什么都不做。

assert依赖于NDEBUG预处理变量状态,所以我们在程序中加入#define NDEBUG就可以关闭assert的检查。

3、bind的简单使用:

auto f=bind(func,a,b,_1,_2)

func是个函数指针或者仿函数对象,反正可以用()调用,叫可调用对象。func调用的时候需要4个参数。

f是新的可调用对象。需要两个参数_1,_2。_1,_2又叫占位符,其作为f的第一个以及第二个参数,被func作为第三与第四参数传入。而a,b是func绑定的第一与第二参数。

以这种用法可以改变参数的排列顺序。非占位符那些变量,是以值拷贝的形式传进func的,而与print的参数类型无关。而占位符代表的参数,在print中怎么传入,在func就怎么传入。

4、标准库ref函数,返回一个对象,包含给定的引用,这个对象是可以拷贝的。所以我们可以通过ref(cin)来获得可拷贝的ostream对象。

5、using:包含using指示与using声明。记住一点,using声明有点像在声明的地方把自己里面的内容放出来,所以一般情况下不允许出现相同的名字,除非是函数名字且名字是重载的才行。而using指示像把内容放到全局空间。在使用using指示的地方出现和命名空间重复的名字,就是隐藏了命名空间中的名字,对于函数,只要出现同名函数,不管参数是否一样,都被隐藏。

6、标准库的function模板,可以代表某种接口的函数。比如,对于int(int,int)是个函数类型,但是它也表示接口。输入两个int且返回一个int。但是这样的接口可以时普通的函数,可以时函数对象,可以是lambda表达式,三种类型都不一样。function可以把接口统一:

function<int(int,int)>就是一个统一的接口。

当上述函数有重载版本时,不能直接插入函数名,一种方法是插入函数指针,或者把函数包装成lambda。

7、bitset是个类模板,与array类似,具有固定的大小。所以定义的时候需要写明包含多少二进制位。既然是模板,则二进制位的数量肯定是常量表达式。

我们通过位置访问每一位,高位下标大,低位下标小。

当用一个整型来初始化bitset的时候,值会被转换为unsigned long long类型,并被当作位模式来处理。如果bitset的大小大于unsigned long long中的二进制位,则剩余填0,如果小于,则只取给定值的低位,高位丢弃。

使用string初始化bitset

bitset的<<运算符是从高位开始输出。

bitset的老多老多函数:any、all、none、count、size、test、set、reset、flip、[]、to_ulong、to_ullong、to_string、<<、>>。

bitset的>>操作从流中读取0/1,先存放到临时的string中,再初始化bitset。如果读取的数量不够位数,高位置零。而超出位数的话,只取前几个,因为前几个是先读取的。

8、随机数

定义在头文件random中的随机数库通过一组协作的类来解决这些问题:随机数引擎类和随机数分布类。一个随机数引擎类生成unsigned范围内的数,一个随机数分布类使用引擎类生成指定类型的、在给定范围内的、服从特定分布的随机数。

随机数发生器指随机数引擎与随机数分布的组合,它每次产生一个相同的序列,所以使用函数产生随机数的时候,引擎和分布要设置为静态。

我们产生随机值最好的方式是使用time函数,利用time返回的时间作为seed来进行随机数的产生。

其他的随机数分布:

9、命名空间什么都能包含,包括其他的命名空间。其自身不能出现在函数或类的内部。

10、定义在命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌的作用域中任何单位访问。外部访问需要指出所用的名字属于哪个空间。

11、#include不放在命名空间内部,因为那样表示头文件的内容都是命名空间的成员。命名空间的成员可以定义在命名空间外,但是必须在所属命名空间的外层空间。模板的特化必须在原始模板所属的命名空间中。

13、对于嵌套的命名空间:内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。

14、嵌套的命名空间可以设置内层命名空间为inline形式的。内联形式的命名空间。内联命名空间中的名字能直接被外层的命名空间使用。

15、未命名的命名空间指直接namespace后加{}。其里面定义的变量都是静态的。而且只能在给定文件内不连续。即两个文件都有未命名的命名空间,这两个命名空间是无关的。

如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。

未命名的命名空间中的名字可以直接使用,作用域与该命名空间所在作用域相同。所以不能有相同的变量。

16、命名空间别名:namespace s=std;

17、using声明:using std::cin;可以出现在全局,局部,命名空间,类作用域中。会隐藏外层的同名实体。

using指示 using namespace std;出现在全局,局部,命名空间作用域。using指示一般被看作出现在最近外层的作用域中,是对使用者有这种感受。

using声明会隐藏外部的同名,而using指示不会:

using指示很多情况:

18、当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外,还会查找实参所属的命名空间。

这个规则可以使得命名空间中类的友元即使没有定义也能被使用,而且也能在命名空间中找到:(我觉得可以不记录)

19、一个using声明引入函数的时候,会与同作用域的函数重载,对外层作用域的函数隐藏。如果同作用域有参数完全一样的函数,则报错

而使用using指示引入函数的重载,即使参数相同也没事,只要显示表明用的到底是哪个。

搞了这么多规则,终于找到了规律:

using声明:在使用声明的作用域中,对变量来说,本作用于不能有与外部作用域相同的变量。声明的变量会隐藏外部的变量。对函数来说,可以有重载版本,但是不能有重复版本。

using指示:其核心是把命名空间成员提升到外层的作用域中。对变量来说,外层作用域可以有与命名空间中相同的变量,但是使用的时候得指明是命名空间中的或外部的。而对于使用指示的作用域内,同名变量就是覆盖命名空间以及外部的变量。对函数也是同理。

20、运行时类型识别:

当将typeid或dynamic_cast用于某种类型的指针或引用且该对象有虚函数的时候,运算符将使用指针或引用所绑定的对象的动态类型。

dynamic_cast的作用是把基类的指针或引用安全转为派生类的指针或引用。

dynamic_cast<type*>(e)

dynamic_cast<type&>(e)

dynamic_cast<type&&>(e)

e要么是公有派生类,要么是公有基类,要么是其本身。而且e要保证其指向的对象至少不能比目标类型小。指针转换出错返回0,而引用转换出错报错,因为没有空引用这种东西。

第二个运行时类型识别是typeid,返回常量对象引用。它唯一忽略的是顶层const。而且对于数组与函数,不会自动转换成指针。

只有在包含对象是一个虚函数类的左值时,其类型才是运行时类型,即动态类型。

注意:typeid一般不会计算()中的值,除非遇到有虚函数的类的左值的对象,才会计算()中的值。记住,如果传入的是对象的 指针,不用管有没有虚函数,都是静态类型的类型。

21、固有不可移植特性:

位域:类可以将非静态成员定义成位域。

位域可以使得几个变量压缩在同一个int中,看机器而定。当取地址符不能用于位域,所以任何指针都无法指向类的位域。

域的操作和一般的变量比较类似,但是记住不要超过它值的范围。

328.第二个固有的不可移植特性是volatile。

它表示的是编译器不应该对这样的对象进行优化。

类可以将成员函数定义成volatile的。只有volatile的对象才能调用volatile的成员。

volatile和const很像,重要的区别是无法使用合成的拷贝/移动构造函数即赋值运算符初始化volatile对象或成volatile对象赋值

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值