C++知识点总结。。。持续更新
函数使用总结
1)值返回的应用场合:算术运算符重载(加号运算符,减号运算符等),引用返回的应用场合:赋值运算符重载。
2)C++将数组名参数视为数组第一个元素的地址。从技术上将,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针;来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是等价的:typeName arr[] ; typeName* arr,这两个声明都表明,arr是指向typeName的指针,但在编写函数代码时,可以像使用数组名那样使用arr来访问元素:arr[i]。
类的使用总结
1)当成员函数的定义直接放在类的定义中时,它们默认是内联函数。但是,如果成员函数在类的定义外部进行定义时,需要使用类的作用域解析运算符 (:😃 来表明该函数属于哪个类,此时它们不会被自动视为内联函数。
2)C++类默认提供的函数:默认构造函数,默认析构函数,复制构造函数,赋值运算符重载,地址运算符重载。
3)复制构造函数:用于创建一个新对象,该对象是以同一类的现有对象作为参数进行初始化的,其形式如下:ClassName(const ClassName&),赋值运算符重载:ClassName& ClassName::operator = (const ClassName&)。
4)基类引用可以指向基类对象和派生类对象。
5)当类的构造函数需要new内存时,其类的复制构造函数和赋值运算符重载函数需要进行深拷贝,不能进行浅拷贝(浅拷贝只拷贝了地址,是不对的)。
6)在类中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类使用被声明的类型,如果类型是在公有部分进行的,则可以从类的外部通过作用域解析符使用被声明的类型创建其对象。
7)常量可以初始化,但不能对它赋值。在概念上说,调用构造函数时,对象将在括号中的代码执行之前被创建。因此,调用类的构造函数将导致程序先给类的成员变量分配内存,然后,程序流程进入括号中,使用常规的赋值方式将值存储到内存中,因此,对应const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化。C++提供了一种特殊的语法来完成上述工作,它叫做成员初始化列表,成员初始化列表由逗号分隔的初始化列表组成(前面带冒号)。它位于参数列表的右括号之后、函数体左括号之前。如果数据成员的名称为mdata,并需要将它初始化为val,则初始化器为mdata(val)。只有构造函数可以使用这种初始化列表语法。对应const类成员,必须使用这种语法,还有被声明为引用的类成员,也必须使用这种语法。这是因为引用与const数据类似,只能在被创建时进行初始化。成员列表初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码。
8)当类对象被按值返回(或传递)时,复制构造函数将被调用。
9)一般来说,类的私有数据成员存储信息,公有成员函数(又称为方法)提供访问数据的唯一途径,类将数据和方法组成一个单元,其私有性实现数据隐藏。
10)this指针是调用对象的地址,因此*this是该对象的别名。
11)使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有成员也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
12)派生类不能直接访问基类的私有成员,而必须通过基类的公有方法来访问私有的基类成员。所以派生类构造函数必须使用基类构造函数。创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。
13)派生类的构造函数重点如下:
- 首先创建基类对象;
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;
- 派生类构造函数应初始化派生类新增的数据成员;
- 如果在初始化成员列表中未指明要使用的基类构造函数,将使用默认的基类构造函数。
14)派生类与基类之间的关系:
- 派生类对象可以使用基类的方法,条件是方法不是私有的;
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象;
- 基类引用可以在不进行显示类型转换的情况下引用派生类对象。
15)基类指针或引用可以指向派生类对象,但派生类指针或者引用不可以指向基类对象。
16)关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。private和protected之间的区别只有在基类派生的类中才会表现处理,派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员类型。
17)最好对类数据成员采用私有访问控制,不要使用保护访问控制,同时使用基类方法使派生类能够访问基类数据。
18)继承与动态分配内存的互动:如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢,这取决于派生类的属性,有以下两种情况(前提都是基类使用了动态内存分配,赋值和复制构造函数在基类中重新进行了定义,析构函数被声明为虚函数):
- 派生类新增成员不需使用new:则析构函数、复制构造函数和赋值运算符都不需要显示定义,直接用派生类默认的析构函数、复制构造函数和复制运算符即可。以复制构造函数为列,当派生类成员复制从基类继承来的成员时,派生类的默认复制构造函数会调用基类的显示构造函数来复制其继承的基类成员。
- 派生类使用了new:则必须为派生类定义显示析构函数、复制构造函数和赋值运算符。派生类析构函数只需释放派生类新增成员new的内存,继承来的基类成员new的内存还是由基类析构函数释放。
派生类复制构造函数的显示生成:通过在初始化成员列表中调用基类的复制构造函数来完成的,举例:baseDMA类有一个私有成员label指针需要new,hasDMA继承自baseDMA类,新增一个私有成员style指针需要new,hasDMA复制构造函数只能访问hasDMA的数据,因此它必须调用baseDMA复制构造函数来处理共享的baseDMA数据,格式如下:
hasDMA::hasDMA(const hasDMA& hs):baseDMA(hs)
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style,hs.style);
}
需要注意一点,成员初始化列表将一个hasDMA引用传递给baseDMA构造函数,没有参数类型为hasDMA引用的baseDMA构造函数,也不需要这样的构造函数,因为baseDMA的复制构造函数有一个baseDMA引用参数,而基类引用可以指向派生类型,因此baseDMA复制构造函数将使用hasDMA参数的baseDMA部分来构造新对象的baseDMA部分。
赋值运算符重载函数的生成:通过在派生类中显示调用基类的显示赋值运算符函数对其继承的基类成员赋值。
19)使用动态内存分配和友元的继承:派生类的友元函数可以直接访问派生类的私有成员,但如何访问从基类继承来的私有成员呢?答案是可以在基类中也定义一个基类的友元函数,然后通过再派生类的友元函数中调用基类的这个友元函数达到访问继承的基类成员的目的。因为友元不是成员函数,所以不能通过作用域解析运算符来指出要使用哪个函数,因此只能使用强制类型转换,以便匹配原型时能够选择正确的函数。因此,派生类的友元函数要想访问基类的友元函数,可以通过将派生类成员通过强制类型转换为基类成员达到访问基类友元函数的目的。
举例如下:基类(baseDMA) 基类友元函数:friend std::ostream& operator<<(std::ostream& os,const baseDMA& bs);
派生类(hasDMA,有私有成员style)派生类友元函数:friend std::ostream& operator<<(std::ostream& os,const hasDMA& hs),其定义如下:
std::ostream& operator<<(std::ostream& os,const hasDMA& hs)
{
os << (const baseDMA&) hs;//将参数const hasDMA&转换成类型为const baseDMA&的参数
os << "style:" << hs.style << endl;
}
20)以公有方式派生的类的对象可以通过多种方式来使用基类的方法:
- 派生类对象自动使用继承而来的基类方法,如果派生类没有重定义该方法。
- 派生类的构造函数自动调用基类的构造函数。
- 派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数。
- 派生类构造函数显示的调用成员初始化列表中指定的基类构造函数。
- 派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法。
- 派生类的友元函数可以通过强制类型转换,将派生类引用或者指针转换成基类引用或指针,然后使用该引用或指针来调用基类的友元函数。
21)explicit:防止单参数构造函数的隐式转换。
22)当初始化类别包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。
23)私有继承注意点:
- 初始化基类组件:构造函数利用成员初始化列表语法初始化基类成员时,要使用类名而不是成员名来标识构造函数。
- 访问基类的方法:使用私有继承时,只能在派生类的方法中使用基类的方法,并且是使用类名和作用域解析符来调用基类的方法。
- 访问基类对象本身:使用强制类型转换,将派生类对象转换为基类对象。
- 访问基类的友元函数:通过显示转换为基类来调用基类的友元函数。
24)使用保护派生或者私有派生时,基类的公有成员将成为保护成员或私有成员,假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法,另一种方法是使用一个using声明(就像名称空间一样,在派生类的public权限下使用using声明)来指出派生类可用使用特定的基类成员,即使采用的是私有派生。
25)多重继承—虚基类:使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象,使用关键字virtual。
26)模板类的代码开头:template或者template。**关键字template告诉编译器,将要定义一个模板,尖括号中的内容相当于函数的参数列表。可用把关键字class和typename看作是变量的类型名,把Type看作是该变量的名称。**可以使用自己的泛型名代替Type,其命名规则与其他标识符相同。当前流行的选项包括T和Type。当模板被调用时,Type将被具体的类型只(如int或者string)取代。在模板定义中,可用使用泛型来标识存储在栈中的类型。
27)泛型编程:允许独立于具体类型来定义算法和数据结构,一般是通过使用模板来实现的,其中类型参数用于在编译时生成特定类型的代码。
28)前向声明:一种在使用类之前声明类的方法。可以应用于解决类之间的相互引用问题(相互引用理解:类A中定义类B对象,类B中声明类A对象)。当两个或多个类相互引用时,可以使用前向声明避免头文件之间的循环依赖。前向声明的语法很简单,只需在使用类之前提供类的简单声明即可,而不需要包含完整的类定义。
29)在C++中,**类的静态成员变量需要在头文件中声明,并且在源文件中进行定义(头文件中声明源文件中定义,必须这样,不然会报错)。**这是因为静态成员变量属于类的一部分,而不是对象的实例。在头文件中定义静态成员变量(即为其分配空间)可能会导致多个源文件中的链接错误。因此,一般建议只在一个源文件中定义静态成员变量。如果多个源文件都需要访问这个静态成员变量,你可以将其定义在一个源文件中,然后在其他源文件中使用 extern
关键字进行声明,以避免链接错误。
30) 当类A是类B的友元时,则类A可以访问类B的私有数据。表现形式如下:
class B
{
friend class A;//在类A内,可以通过类B对象来访问类B的私有成员
}
31)在C++中,可以将类声明放在另一个类中,在另一个类中声明的类被称为嵌套类,它通过提供新的类型作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象,而仅当声明位于公有部分,才能包含类的外面使用嵌套类,而且必须使用作用域解析运算符。
32)对类进行嵌套和包含的区别:包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类对象,而是定义一种类型,该类型尽在包含嵌套类声明的类中有效。
虚函数使用总结
虚函数实现机制详细解释参考链接:
C++虚函数表实现机制
C++的对象内存布局-虚函数实现
1)虚函数的工作原理:虚函数表和虚函数指针。基类对象包含一个指针,该指针会指向基类中所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存原始版本的地址,如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚函数表中。注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。对应每个类,编译器都创建一个虚函数地址表(数组)。
- 虚函数指针:在含有虚函数的类对象中创建时生成虚函数指针(内存为4个字节,是一个指向指针数组的指针,指针数组就是该类的虚函数表),它指向该类的虚函数表。。
- 虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。只要含有虚函数的类都有自己的虚函数表。
- 同一个类的不同实列共用一份虚函数表,它们都通过一个所谓的虚函数表
2)使用虚函数时,在内存和执行速度方面由一定的成本,包括:
- 每个对象都将增大,增大量为存储地址的空间;
- 对于每个类,编译器都创建一个虚函数地址表(数组);
- 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
3)如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法,这就称为动态联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
4)构造函数不能为虚函数的原因:因为创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。还有一种解释:因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针。
5)友元函数不能是虚函数的原因:因为友元不是类成员,而只有成员才能使虚函数。
6)如果派生类没有重新定义虚函数,将使用该函数的基类版本,如果派生类位于派生链中,则将使用最小的虚函数版本,列外的情况是基类版本是隐藏的。
7)如果基类析构函数不是虚的,则将只调用对应于基类指针类型的析构函数,但基类析构函数为虚的,将会调用指向的派生类对象类型的析构函数,然后自动调用基类的析构函数。
8)虚函数和纯虚函数:
- 纯虚函数:函数声明的结尾处要=0,但在类中不定义该函数。当类声明中包含纯虚函数时,则不能创建该类的对象,包含纯虚函数的类只用作基类。
- 虚函数在子类里面可以不重写,当子类不重写虚函数时,则会直接继承基类中的虚函数的实现。这意味着,当通过基类指针或引用调用虚函数时,会执行基类中定义的实现。但纯虚函数必须在子类实现才可以实列化子类。
- 虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。
- 至少含有一个纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
- 所有函数都是纯虚函数的类叫做纯虚类。
标准库模板
1)STL提供了一组表示容器、迭代器、函数对象和算法的模板。容器是一个与数组类似的单元,可以存储若干个值。STL容器是同质的,即存储的值的类型相同;算法是完成特定任务(如对数组进行排序或在链表中查找特定值)<