C/C++知识点总结

文章目录

        • 1.引用
        • 2.static关键字
        • 3.const关键字
        • 4.inline内联函数
        • 5.函数重载
        • 6.作用域描述符 ::
        • 7.强制类型转换
        • 8.类与对象
        • 9.构造函数
        • 10.拷贝构造函数
        • 11.深拷贝与浅拷贝
        • 12.析构函数
        • 13.静态成员
        • 14.友元函数与友元类
        • 15.常对象与常对象成员
        • 16.继承与派生
        • 17.虚基类
        • 18.多态性
        • 19.虚函数
        • 20.纯虚函数
        • 21.抽象类
        • 22.虚析构函数
        • 23.重载运算符
        • 24.函数模板与类模板
        • 25.异常处理
        • 26.STL:标准模板库
        • 26. STL容器
        • 27. 迭代器
        • 28.类的类型转换与explicit

期末复习、笔试、面试硬核知识点:

先赞后看养成好习惯

C/C++知识点总结_第1张图片

1.引用

//类型 &引用名 = 已定义的变量名
#include 
using namespace std;
int main() 
{
	int i = 10;
	int &j = i;
	cout << "i = " << i << " j = " << j << endl;
	cout << "i的地址为 " << &i << endl;
	cout << "j的地址为 " << &j << endl;
	return 0;
}

主要:

  1. 内存:引用与其所代表的变量共享同一内存单元,系统并不为引用另外分配存储空间。也就是说,它是某个已存在变量的另一个名字。
  2. 初始化:引用并不是一种独立的数据类型,它必须与某一种类型的变量相联系。在声明引用时,必须立即对它进行初始化,不能声明完成后再赋值,之后也不能改变指向。为引用提供的初始值,可以是一个变量或者另一个引用。

进一步说明:

  1. 不允许建立void类型的引用
  2. 不能建立引用的数组
  3. 不能建立引用的引用。不能建立指向引用的指针。引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针。
  4. 可以将引用的地址赋值给一个指针,此时指针指向的是原来的变量。
  5. 可以用const对引用加以限定,不允许改变该引用的值,但是它不阻止引用所代表的变量的值。

相关面试问题:
50.引用和指针的区别
引用是一个变量的别名,指针是一个变量,存的另一个变量的地址。
引用不能为空引用,指针可以是空指针。
引用赋值之后不可以改变指向,指针可以改变。
引用不能单独存在,指针可以。
引用必须进行初始化,指针可以随时初始化。

51.函数参数传递中值传递、地址传递、引用传递有什么区别?
值传递,形参的改变不影响实参,实参只是将值传递过去,实际上实参的地址与形参的地址还是两块地址,所以改变其中一个,不会影响另一个。
地址传递,形参的改变会影响实参,形参中存的是实参的地址,改变形参中的值,实际上是改变它存的那块地址里的值。
引用传递,形参的改变也会影响实参, 形参是实参的别名,形参和实参是同一个地址,所以改变一个必然会影响另一个。

2.static关键字

static 关键字的作用

  1. 修饰函数:函数用static修饰,改变了作用域。普通的函数是可以通过头文件声名的方式被其他文件调用,被static修饰后就只能在本文件里被调用,这样是为了数据的安全。
    作用:有些函数并不想对外提供,只需要在本文件里调用,这时候就可以用static去修饰。
    总结:改变了作用域,没有改变其生存周期。
  2. 修饰全局变量:全局变量用static修饰改变了作用域,没有改变生存周期。普通的全局变量是可以被其他的.c文件extern引用的,一旦被static修饰,就只能被定义该全局变量的.c文件引用,使得该全局变量的作用范围减小。
    作用:当一个全局变量不想被其他.c文件引用时,可以用static修饰,这样其他的文件就不能通过extern的方式去访问,这样主要是为了数据安全。
    总结:改变其作用域,没有改变生存周期。
  3. 修饰局部变量:局部变量就是在函数内定义的变量,普通的局部变量,生存周期是随着函数的结束而结束,每次函数重新执行,局部变量都是新的值,不会保留上次的值。当用static修饰后,局部变量的生存周期就是当程序结束才会结束。再次调用函数时,用static修饰的变量会保留上一次的值。
    应用:在函数内,我们想保留某些变量上一次的值,就可以用static去修饰该变量。比如:想统计该函数被执行的次数时,就可以定义被static修饰的int型变量,每执行一次该变量就++。
    总结:用static修饰的局部变量,改变了生存周期,但是没有改变其作用域。改变其生存周期的原因是被static修饰的局部变量被存放在.bss段或者.data段,而普通的局部变量是存放在栈上的。

3.const关键字

53.const关键字有什么作用?
const修饰之后变为“只读”。
变量被const修饰变为常量,值被初始化后,不能再修改。

  1. 常函数const修饰在形参列表的后面,常函数中的成员变量不能被修改。
  2. 常对象,对象中的成员属性不能被修改。
  3. 常引用,引用中的属性值不能被修改。
  4. const可以与指针一起使用,它们的组合情况复杂,可归纳为3种:指向常量的指针(一个指向常量的指针变量,不允许改变指针所指的变量,但可以改变指针的指向)、常指针(将指针变量所指的地址声明为常量,不能改变指针的指向,但可以改变指针指向的地址的值)和指向常量的常指针(指针所指的地址不能改变,它所指向的地址中的内容也不能改变)。
  5. mutable 关键字可以打破const限制

4.inline内联函数

内联函数
在函数名前冠以关键字inline,该函数就被声明为内联函数。每当程序中出现对该函数的调用时,C++编译器使用函数体中的代码插入到调用该函数的语句之处,同时使用实参代替形参,以便在程序运行时不再进行函数调用。引入内联函数主要是为了消除调用函数时的系统开销,以提高运行速度。
说明:

  1. 内联函数在第一次被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码
  2. 在内联函数体内一般不能含有复杂的控制语句,如for语句和switch语句等
  3. 使用内联函数是一种空间换时间的措施,若内联函数较长,较复杂且调用较为频繁时不建议使用
  4. 使用内联函数替代宏定义,能消除宏定义的不安全性

5.函数重载

  1. 在同一作用域内,只要函数参数的类型不同,或者参数的个数不同,或者二者兼而有之,两个或者两个以上的函数可以使用相同的函数名。
  2. 调用重载函数时,函数返回值类型不在参数匹配检查之列。因此,若两个函数的参数个数和类型都相同,而只有返回值类型不同,则不允许重载。
  3. 函数的重载与带默认值的函数一起使用时,有可能引起二义性。
void Drawcircle(int r = 0, int x = 0, int y = 0);
void Drawcircle(int r);
Drawcircle(20);
  1. 在调用函数时,如果给出的实参和形参类型不相符,C++的编译器会自动地做类型转换工作。如果转换成功,则程序继续执行,在这种情况下,有可能产生不可识别的错误。

6.作用域描述符 ::

  1. 通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。
  2. 如果希望在局部变量的作用域内使用同名的全局变量,可以在该变量前加上“::”,此时::value代表全局变量value,“::”称为作用域标识符。

7.强制类型转换

可用强制类型转换将不同类型的数据进行转换。例如,要把一个整型数(int)转换为双精度型数(double),可使用如下的格式:

int i = 10;
double x = (double)i;int i = 10;
double x = double(i);

以上两种方法C++都能接受,建议使用后一种方法。

8.类与对象

通常把具有共同属性行为的事物所构成的集合称为类。
类的对象可以看成该类类型的一个实例,定义一个对象和定义一个一般变量相似。

  1. 类的构成
    类声明中的内容包括数据和函数,分别称为数据成员和成员函数。按访问权限划分,数据成员和成员函数又可分为公有有、保护和私有三种。
class 类名{
    public:
        公有数据成员;
        公有成员函数;
    protected:
        保护数据成员;
        保护成员函数;
    private:
        私有数据成员;
        私有成员函数;
};
1.对一个具体的类来讲,类声明格式中的3个部分并非一定要全有,但至少要有其中的一个部分。一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。这样,内部的数据整个隐蔽在类中,在类的外部根本就无法看到,使数据得到有效的保护,也不会对该类以外的其余部分造成影响,程序之间的相互作用就被降低到最小。
2.private: 只能被类成员函数及友元访问,不能被其他任何访问,本身的类对象也不行。
  protected: 只能被类成员函数、子类函数及友元访问,不能被其他任何访问,本身的类对象也不行。
  public: 能被类成员函数、子类函数、友元访问,也能被类的对象访问。
在没有继承的情况下,protectedprivate相同。在派生类的时候才出现分化,子类的成员函数不能直接访问父类的private成员,只有通过父类的public函数来间接访问,不是很方便;一个类, 如果希望,它的成员, 可以被自己的子类(派生类)的成员函数直接访问,但是,又不想被外部访问那么就可以把这些成员定义为 protected访问权限。
3.类声明中的关键字privateprotectedpublic可以任意顺序出现。
4.若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。
5.c++11标准之后可以在类内给数据成员赋初始值,虽然在一定程度上这样写破坏了类的抽象性,但是却能带来一定的便利。但这也是抽象出来共有的属性啊,并且这样写后,初始化列表初始化的变量值覆盖掉声明时初始化的值;构造函数中初始化的值又会覆盖掉初始化列表的值。
  1. 类的作用域和类成员的访问属性
    私有成员只能被类中的成员函数访问,不能在类的外部,通过类的对象进行访问。
    一般来说,公有成员是类的对外接口,而私有成员是类的内部数据和内部实现,不希望外界访问。将类的成员划分为不同的访问级别有两个好处:一是信息隐蔽,即实现封装,将类的内部数据与内部实现和外部接口分开,这样使该类的外部程序不需要了解类的详细实现;二是数据保护,即将类的重要信息保护起来,以免其他程序进行不恰当的修改。
  2. 说明
    在类的内部所有成员之间都可以通过成员函数直接访问,但是类的外部不能访问对象的私有成员。
    在定义对象时,若定义的是指向此对象的指针变量,则访问此对象的成员时,不能用“.”操作符,而应该使用“->“操作符。

9.构造函数

构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。构造函数的名字必须与类名相同,而不能由用户任意命名。它可以有任意类型的参数,但不能具有返回值。它不需要用户来调用,而是在建立对象时自动执行。

  1. 构造函数一般声明为共有成员,但它不需要也不能像其他成员函数那样被显式地调用,它是在定义对象的同时被自动调用,而且只执行一次。
  2. 构造函数的名字必须与类名相同,否则编译程序将把它当做一般的成员函数来处理。
  3. 构造函数没有返回值,在定义构造函数时,是不能说明它的类型的。
  4. 与普通的成员函数一样,构造函数的函数体可以写在类体内,也可写在类体外。
  5. 构造函数可以不带参数,可以带默认参数。
  6. 构造函数可以重载,当无参数的构造函数和带默认参数的构造函数重载时,有可能产生二义性。
    默认的构造函数:
  7. 如果没有给类定义构造函数,则编译系统自动生成一个默认的构造函数。
  8. 只要一个类定义了一个构造函数(有参或着无参),系统将不再给它提供默认的构造函数。
  9. 对没有定义构造函数的类,其公有数据成员可以用初始值列表进行初始化。

10.拷贝构造函数

拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用是在建立一个新对象时,使用一个已存在的对象去初始化这个新对象。

拷贝构造函数具有以下特点:

  1. 因为拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值。
  2. 拷贝构造函数只有一个参数,并且是同类对象的引用。
  3. 每个类都必须有一个拷贝构造函数。可以自己定义拷贝构造函数,用于按照需要初始化新对象;
  4. 如果没有定义类的拷贝构造函数,系统就会自动生成一个默认拷贝构造函数,用于复制出与数据成员值完全相同的新对象。
class Score
{
public:
	Score(int m, int f);  //构造函数
	Score();
	Score(const Score &p);  //拷贝构造函数
private:
	int mid_exam;
	int fin_exam;
}
Score::Score(const Score &p)
{
	mid_exam = p.mid_exam;
	fin_exam = p.fin_exam;
}

自动调用拷贝构造函数的三种情形:
1.当用一个已有对象给另一个同类的对象进行初始化时;
2.当一个函数的参数是一个类的对象,调用函数形参和实参结合时;
3.当一个函数的返回值是一个类的对象,函数执行完成返回调用者时。

11.深拷贝与浅拷贝

浅拷贝

  1. 浅拷贝会创建一个新的对象,新对象会拷贝原始对象的属性,如果原始对象的属性是基本数据类型的值,则新对象就拷贝这个值,如果原始对象的属性是内存地址(引用类型),那么新对象就拷贝这个地址,(也就是说:其中一个对象改变了这个地址,就会影响到另一个对象。)
  2. 如果没有创建一个拷贝构造函数,那么此时系统就会自动生成一个缺省拷贝构造函数,这个缺省拷贝构造函数的功能就是把原始对象的每个数据成员的值都复制到新建立的对象中。 也就是浅拷贝!
  3. 浅拷贝是对对象的数据成员的简单复制。
  4. 注意:如果原始对象的数据成员属性是指针类型,内存地址(引用类型),浅拷贝后,如果其中一个对象的内存空间被释放时,其他拷贝的指针都成了野指针。而且作用域结束,释放内存时,同一块内存可能被释放两次,造成错误。

当需要拷贝的对象有其他资源时(比如动态内存空间,文件…),浅拷贝是无法实现拷贝的需求,此时就需要深拷贝的帮助!

深拷贝:

  1. 在拷贝之前,拷贝对象会为指针类型的数据成员另开辟一个独立的内存空间,也就是实现真正内容上的拷贝。这就是深拷贝。
  2. 拷贝对象会重新创建一块内存空间,与原始对象是独立开的
  3. 其中任何一个对象的改动都不会对另外一个对象造成影响。
class Person
{
	char* name;
public:
	Person(const Person& p);
}
Person::Person(const Person& p)
{
	name = new char[strlen(p.name)+1];
	strcpy(name,p.name);
}

总结:
在C++中,拷贝构造函数用于一个对象初始化另一个对象。如果我们没有定义拷贝构造函数,则系统会自动创建一个默认的拷贝构造函数,用于浅拷贝,即简单的拷贝对象。如果原始对象拥有动态内存,此时拷贝对象就会被赋值为原始对象的内存地址,这样就意味着两个对象指向了同一块内存空间。那么当析构函数被调用时,动态内存会被释放掉。但这就会导致内存的被释放两次,出现异常报错!为了解决这个问题,我们会自己创建一个构造函数,动态申请内存,将原始对象的内容完整的复制过来,这个过程就是深拷贝!

68.深拷贝与浅拷贝的区别
当类中有指针成员时,浅拷贝只将指针中保存的地址复制给新的对象,实际上还是指的同一块内存,当析构函数对两个对象进行内存释放时会出现double free异常(释放第一个对象时,已将内存删除,第二个对象释放时还是找的原有地址,但内存已不存在)。
深拷贝拷贝时,先开辟一个新的与原对象动态空间大小相同的空间,将原对象指针中指向内存的数据进行拷贝,新对象指针存储新内存的地址,原对象指针存储原内存的地址,所以释放时互不干扰。

12.析构函数

析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,析构函数是在对象自动销毁时被系统调用的函数,作用是释放对象中开辟的内存空间,防止内存泄漏。

  1. 析构函数与构造函数名字相同,但它前面必须加一个波浪号(~)。
  2. 析构函数没有参数和返回值,也不能被重载,因此只有一个。
  3. 当撤销对象时,系统会自动调用析构函数。

13.静态成员

静态数据成员:
在一个类中,若将一个数据成员说明为static,则这种成员被称为静态数据成员。与一般的数据成员不同,无论建立多少个类的对象,都只有一个静态数据成员的拷贝。从而实现了同一个类的不同对象之间的数据共享。
定义静态数据成员的格式如下:static 数据类型 数据成员名;

  1. 静态数据成员属于类,(准确地说,是属于类中对象的集合),初始化应在定义对象之前进行。
  2. 静态数据成员属于类(准确地说,是属于类中对象的集合),而不像普通数据成员那样属于某一对象,因此,公有,可以使用“类名::”访问静态的数据成员。

静态成员函数:

在类定义中,前面有static修饰的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。定义静态成员函数的格式如下:
static 返回类型 静态成员函数名(参数表);

  1. 一般情况下,静态函数成员主要用来访问静态成员函数。当它与静态数据成员一起使用时,达到了对同一个类中对象之间共享数据的目的。
  2. 私有静态成员函数不能被类外部的函数和对象访问。
  3. 使用静态成员函数的一个原因是,可以用它在建立任何对象之前调用静态成员函数,以处理静态数据成员,这是普通成员函数不能实现的功能

14.友元函数与友元类

友元函数:

  1. 友元函数既可以是不属于任何类的非成员函数,也可以是另一个类的成员函数。友元函数不是当前类的成员函数,但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。
  2. 在类中声明友元函数时,需要在其函数名前加上关键字friend。此声明可以放在公有部分,也可以放在保护部分和私有部分。友元函数可以定义在类内部,也可以定义在类外部。
  3. 将非成员函数声明为友元函数:
    a. 友元函数虽然可以访问类对象的私有成员,但他可以不是成员函数。因此,在类的外部定义友元函数时,不必像成员函数那样,在函数名前加上“类名::”。
    b. 因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
    c. 友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。
//一个函数同时定义为两个类的友元函数
#include 
#include 
using namespace std;
class Score;    //对Score类的提前引用说明
class Student{
private:
	string name;
	int number;
public:
	Student(string na, int nu) {
		name = na;
		number = nu;
	}
	friend void show(Score &sc, Student &st);
};
class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f) {
		mid_exam = m;
		fin_exam = f;
	}
	friend void show(Score &sc, Student &st);
};
void show(Score &sc, Student &st) {
	cout << "姓名:" << st.name << "  学号:" << st.number << endl;
	cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}
int main() {
	Score sc(89, 99);
	Student st("白", 12467);
	show(sc, st);

	return 0;
}

  1. 将成员函数声明为友元函数:
    一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。
#include 
#include 
using namespace std;
class Score;    //对Score类的提前引用说明
class Student{
private:
	string name;
	int number;
public:
	Student(string na, int nu) {
		name = na;
		number = nu;
	}
	void show(Score &sc);
};
class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f) {
		mid_exam = m;
		fin_exam = f;
	}
	friend void Student::show(Score &sc);
};
void Student::show(Score &sc) {
	cout << "姓名:" << name << "  学号:" << number << endl;
	cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}
int main() {
	Score sc(89, 99);
	Student st("白", 12467);
	st.show(sc);

	return 0;
}

友元类:

可以将一个类声明为另一个类的友元

class Y{
    ···
};
class X{
    friend Y;    //声明类Y为类X的友元类
};
  1. 当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,这就意味着作为友元类中的所有成员函数都可以访问另一个类中的所有成员。
  2. 友元关系不具有交换性和传递性。

15.常对象与常对象成员

常对象:
如果在说明对象时用const修饰,则被说明的对象为常对象。
常对象中的数据成员为常量且必须要有初值。

const Date date(2021, 5, 31);

常对象成员:

  1. 常数据成员:
    a.类的数据成员可以是常量或常引用,使用const说明的数据成员称为常数据成员。如果在一个类中说明了常数据成员,那么构造函数就只能通过成员初始化列表对该数据成员进行初始化,而任何其他函数都不能对该成员赋值。
    b.一旦某对象的常数据成员初始化后,该数据成员的值是不能改变的。
class Date{
private:
	const int year;
	const int month;
	const int day;
public:
	Date(int y, int m, int d) : year(y), month(m), day(d) {}
};
  1. 常成员函数
    a.const是函数类型的一个组成部分,因此在声明函数和定义函数时都要有关键字const。在调用时不必加const。
    b.关键字const可以被用于对重载函数进行区分。
class Date{
private:
	int year;
	int month;
	int day;
public:
	Date(int y, int m, int d) : year(y), month(m), day(d){
	}
	void showDate();
	void showDate() const;
};
void Date::showDate() {
	//···
}
void Date::showDate() const {
	//···
}

说明:

  1. 常成员函数可以访问常数据成员,也可以访问普通数据成员。
  2. 常对象只能调用它的常成员对象,而不能调用普通成员函数。常成员函数是常对象唯一的对外接口。
  3. 常对象函数不能更新对象的数据成员,也不能调用该类的普通成员函数,这就保证了在常成员函数中绝不会更新数据成员的值。

16.继承与派生

  1. 继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类或父类,在它基础上建立的新类称为派生类或子类。
  2. 类的继承是新的类从已有类那里得到已有的特性。,从已有类产生新类的过程就是类的派生
  3. 关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。
  4. 构造函数的调用严格地按照先调用基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。
  5. 在定义派生类对象时,构造函数的调用顺序如下:
    a.调用基类的构造函数,对基类数据成员初始化。
    b.调用子对象的构造函数,对子对象的数据成员初始化。
    c.调用派生类的构造函数体,对派生类的数据成员初始化。
  6. 派生类构造函数的一般格式为:
    派生类名(参数总表):基类名(参数表)
    {
    派生类新增数据成员的初始化语句
    }
  7. a.当基类构造函数不带参数时,派生类不一定需要定义构造函数;然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数,甚至所定义的派生类构造函数的函数体可能为空,它仅仅起参数的传递作用。
    b.若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)”,此时若派生类也不需要构造函数,则可不定义构造函数。
    c.如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。

17.虚基类

虚基类的作用:如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。

#include 
#include 
using namespace std;
class Base{
protected:
	int a;
public:
	Base(){
		a = 5;
		cout << "Base a = " << a << endl;
	}
};
class Base1: public Base{
public:
	Base1() {
		a = a + 10;
		cout << "Base1 a = " << a << endl;
	}
};
class Base2: public Base{
public:
	Base2() {
		a = a + 20;
		cout << "Base2 a = " << a << endl;
	}
};
class Derived: public Base1, public Base2{
public:
	Derived() {
		cout << "Base1::a = " << Base1::a << endl;
		cout << "Base2::a = " << Base2::a << endl;
	}
};
int main() {
	Derived obj;
	return 0;
}


//运行结果
Base a = 5
Base1 a = 15
Base a = 5
Base2 a = 25
Base1::a = 15
Base2::a = 25

虚基类的声明:
不难理解,如果在上列中类base只存在一个拷贝(即只有一个数据成员a),那么对a的访问就不会产生二义性。在C++中,可以通过将这个公共的基类声明为虚基类来解决这个问题。这就要求从类base派生新类时,使用关键字virtual将base声明为虚基类。
修改代码:

class Base1:virtual public Base{
public:
	Base1() {
		a = a + 10;
		cout << "Base1 a = " << a << endl;
	}
};

class Base2:virtual public Base{
public:
	Base2() {
		a = a + 20;
		cout << "Base2 a = " << a << endl;
	}
};
//运行结果:
Base a = 5
Base1 a = 15
Base2 a = 35
Base1::a = 35
Base2::a = 35

18.多态性

  1. 所谓多态性就是不同对象收到相同的消息时,产生不同的动作。这样,就可以用同样的接口访问不同功能的函数,从而实现“一个接口,多种方法”。
  2. 从实现的角度来讲,多态可以划分为两类:编译时的多态和运行时的多态。在C++中,多态的实现和连编这一概念有关。所谓连编就是把函数名与函数体的程序代码连接在一起的过程。静态连编就是在编译阶段完成的连编。编译时的多态是通过静态连编来实现的。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。运行时的多态是用动态连编实现的。动态连编时运行阶段完成的,即当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。
  3. 在C++中,编译时多态性主要是通过函数重载和运算符重载实现的;运行时多态性主要是通过虚函数来实现的。

多态目的:实现逻辑传递。
实现的方法:子类继承父类,重写父类中的虚函数。父类指针指向子类对象时,可以通过父类的指针调用子类中重写的函数。
多态存在的3个条件
1)有继承
2)有重写(虚函数)
3)父类指针或引用指向子类对象

19.虚函数

  1. 虚函数的定义是在基类中进行的,基类中需要定义为虚函数的成员函数,声明中冠以关键字virtual;
  2. 在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
  3. 在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写也可以不写。
  4. **只有通过基类指针访问虚函数时才能获得运行时的多态性。**当基类指针(引用)指向派生类对象时,调用一个基类和派生类都有的函数,如果基类的是虚函数,那么调用的是派生类覆盖(重写)后的,如果不是虚函数则调用基类原有的.
  5. a.虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
    b.内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做非内联的。
    c.构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常说明为虚函数。

20.纯虚函数

  1. 纯虚函数是在声明虚函数时被“初始化为0的函数”,声明纯虚函数的一般形式如下:
    virtual 函数类型 函数名(参数表) = 0;
  2. 声明为纯虚函数后,基类中就不再给出程序的实现部分。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要重新定义。
  3. 纯虚函数在j基类中没有具体实现,具体的实现由派生类重写实现。
  4. 带有纯虚函数的类叫抽象类,抽象类不能实例化对象。抽象类是专门被继承的,纯虚函数是专门被派生类重写的。

21.抽象类

如果一个类至少有一个纯虚函数,那么就称该类为抽象类,对于抽象类的使用有以下几点规定:

  1. 由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象。
  2. 不允许从具体类派生出抽象类。所谓具体类,就是不包含纯虚函数的普通类。
  3. 抽象类不能用作函数的参数类型、函数的返回类型、显式转换的类型。
  4. 可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
  5. 如果派生类中没有定义纯虚函数的实现,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。
  6. 如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

22.虚析构函数

如果在主函数中定义一个基类的对象指针,用new运算符建立一个派生类的匿名对象,并将匿名对象的地址赋值给这个对象指针,当用delete运算符撤销匿名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。

Base *p;
p = new Derived;
delete p;
-----------------
输出:调用基类Base的析构函数
原因是当撤销指针p所指的派生类的无名对象,而调用析构函数时,采用了静态连编方式,只调用了基类Base的析构函数。
  1. 如果希望程序执行动态连编方式,在用delete运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。
  2. 虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该类所派生的所有派生类的析构函数也都自动成为虚函数。
#include 
#include 
using namespace std;

class Base{
public:
	virtual ~Base() {
		cout << "调用基类Base的析构函数..." << endl;
	}
};

class Derived: public Base{
public:
	~Derived() {
		cout << "调用派生类Derived的析构函数..." << endl;
	}
};

int main() {
	Base *p;
	p = new Derived;
	delete p;
	return 0;
}
-----------------
调用派生类Derived的析构函数...
调用基类Base的析构函数...

23.重载运算符

  1. 如果不做特殊处理,C++ 的 +、-、*、/ 等运算符只能用于对基本类型的常量或变量进行运算,不能用于对象之间的运算。
  2. 运算符重载,就是对已有的运算符赋予多重含义,使同一运算符作用于不同类型的数据时产生不同的行为。运算符重载的目的是使得 C++ 中的运算符也能够用来操作对象
  3. 运算符重载的实质是编写以运算符作为名称的函数。不妨把这样的函数称为运算符函数。运算符函数的格式如下:
    返回值类型 operator 运算符(形参表(const 修饰的引用或指针类型))
    {

    }
  4. 包含被重载的运算符的表达式会被编译成对运算符函数的调用,运算符的操作数成为函数调用时的实参,运算的结果就是函数的返回值。运算符可以被多次重载。

运算符重载为友元函数:
一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为全局函数又不能访问类的私有成员,因此需要将运算符重载为友元。

例如,对于复数类 Complex 的对象,希望它能够和整型以及实数型数据做四则运算,假设 c 是 Complex 对象,希望c+5和5+c这两个表达式都能解释得通。

将+重载为 Complex 类的成员函数能解释c+5,但是无法解释5+c。要让5+c有意义,则应对+进行再次重载,将其重载为一个全局函数。为了使该全局函数能访问 Complex 对象的私有成员,就应该将其声明为 Complex 类的友元。具体写法如下:

class Complex
{
    double real, imag;
public:
    Complex(double r, double i):real(r), imag(i){};
    Complex operator + (double r);
    friend Complex operator + (double r, const Complex & c);
};
Complex Complex::operator + (double r)
{ //能解释c+5
    return Complex(real+r, imag);
}
Complex operator + (double r, const Complex & c)
{ //能解释5+c
    return Complex (c.real+r, c.imag);
}

重载++和–(自增和自减运算符)
自增运算符++、自减运算符–都可以被重载,但是它们有前置、后置之分。
以++为例,假设 obj 是一个 CDemo 类的对象,++obj和obj++本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++运算符:
CDemo & CDemo::operator ++ ()
{
//…
return * this;
}
那么不论obj++还是++obj,都等价于obj.operator++()无法体现出差别。
为了解决这个问题,C++ 规定,在重载++或–时,允许写一个增加了无用 int 类型形参的版本,编译器处理++或–前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:

class CDemo {
private:
	int n;
public:
	CDemo(int i=0):n(i) { }
	CDemo & operator++(); //用于前置形式
	CDemo operator++( int ); //用于后置形式
	
	friend CDemo & operator--(CDemo & );
	friend CDemo operator--(CDemo & ,int);
};
CDemo & CDemo::operator++()
{//前置 ++
	n ++;
	return * this;
}
CDemo CDemo::operator++(int k )
{ //后置 ++
	CDemo tmp(*this); //记录修改前的对象
	n++;
	return tmp; //返回修改前的对象
}
CDemo & operator--(CDemo & d)
{//前置--
	d.n--;
	return d;
}
CDemo operator--(CDemo & d,int)
{//后置--
	CDemo tmp(d);
	d.n --;
	return tmp;
}

在 C++ 中进行运算符重载时,有以下问题需要注意:

  1. 重载后运算符的含义应该符合原有用法习惯。例如重载+运算符,完成的功能就应该类似于做加法,在重载的+运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
  2. C++ 规定,运算符重载不改变运算符的优先级。
  3. 以下运算符不能被重载:.、.*、::、? :、sizeof。
  4. 重载运算符()、[]、->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。

24.函数模板与类模板

函数模板:
所谓函数模板,实际上是建立一个通用函数,其函数返回类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板。在调用函数时,系统会根据实参的类型(模板实参)来取代模板中的虚拟类型,从而实现不同函数的功能。

#include 
using namespace std;

template <typename T>
T Max(T *array, int size = 0) {
	T max = array[0];
	for (int i = 1	; i < size; i++) {
		if (array[i] > max) max = array[i];
	}
	return max;
}

int main() {
	int array_int[] = {783, 78, 234, 34, 90, 1};
	double array_double[] = {99.02, 21.9, 23.90, 12.89, 1.09, 34.9};
	int imax = Max(array_int, 6);
	double dmax = Max(array_double, 6);
	cout << "整型数组的最大值是:" << imax << endl;
	cout << "双精度型数组的最大值是:" << dmax << endl;
	return 0;
}

实际上,template是一个声明模板的关键字,它表示声明一个模板。类型参数(通常用C++标识符表示,如T、type等)实际上是一个虚拟的类型名,使用前并未指定它是哪一种具体的类型,但使用函数模板时,必须将类型实例化。类型参数前需加关键字typename或class,typename和class的作用相同,都是表示一个虚拟的类型名(即类型参数)。

注意:

  1. 在函数模板中允许使用多个类型参数。但是,应当注意template定义部分的每个类型参数前必须有关键字typename或class。
  2. 在template语句与函数模板定义语句之间不允许插入别的语句
  3. 同一般函数一样,函数模板也可以重载。
  4. 函数模板与同名的非模板函数可以重载。在这种情况下,调用的顺序是:首先寻找一个参数完全匹配的非模板函数,如果找到了就调用它;若没有找到,则寻找函数模板,将其实例化,产生一个匹配的模板参数,若找到了,就调用它。

类模板:
所谓类模板,实际上就是建立一个通用类,其数据成员、成员函数的返回类型和形参类型不具体指定,用一个虚拟的类型来代表。使用类模板定义对象时,系统会根据实参的类型来取代类模板中虚拟类型,从而实现不同类的功能。

template <typename T>
class Three{
private:
    T x, y, z;
public:
    Three(T a, T b, T c) {
        x = a; y = b; z = c;
    }
    T sum() {
        return x + y + z;
    }
}

类模板中的成员函数也可以在类模板体外定义:

  1. 需要在成员函数定义之前进行模板声明;
  2. 在成员函数名前要加上“类名<类型参数>::”;
template<typename T>
T Three<T>::sum() {
    return x + y + z;
}

25.异常处理

程序中常见的错位分为两大类:编译时错误和运行时错误。编译时的错误主要是语法错误,如关键字拼写错误、语句末尾缺分号、括号不匹配等。运行时出现的错误统称为异常,对异常的处理称为异常处理。

C++处理异常的办法:如果在执行一个函数的过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用函数)来解决,如果上一级函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上传,如果到最高一级还无法处理,运行系统一般会自动调用系统函数terminate(),由它调用abort终止程序。

例子:输入三角形的三条边长,求三角形的面积。当输入边的长度小于0时,或者当三条边都大于0时但不能构成三角形时,分别抛出异常,结束程序运行。

#include 
#include 
using namespace std;

double triangle(double a, double b, double c) {
	double s = (a + b + c) / 2;
	if (a + b <= c || a + c <= b || b + c <= a) {
		throw 1.0;        //语句throw抛出double异常
	}
	return sqrt(s * (s - a) * (s - b) * (s - c));
}

int main() {
	double a, b, c;
	try {
		cout << "请输入三角形的三个边长(a, b, c): " << endl;
		cin >> a >> b >> c;
		if (a < 0 || b < 0 || c < 0) {
			throw 1;   //语句throw抛出int异常
		}
		while (a > 0 && b > 0 && c > 0) {
			cout << "a = " << a << " b = " << b << " c = " << c << endl;
			cout << "三角形的面积 = " << triangle(a, b, c) << endl;
			cin >> a >> b >> c;
			if (a <= 0 || b <= 0 || c <= 0) {
				throw 1;
			}
		}
	} catch (double) {
		cout << "这三条边不能构成三角形..." << endl;
	} catch (int) {
		cout << "边长小于或等于0..." << endl;
	}
	return 0;
}

26.STL:标准模板库

STL Standard Template Library,即标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能。 该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。
STL中六大组件:
STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的。

STL的组成 含义
容器 一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。
算法 STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件 中。
迭代器 在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。
函数对象 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)
适配器 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。
内存分配器 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。
相关头文件:
<iterator>	<functional>	<vector>	<deque>
<list>	<queue>	<stack>	<set>
<map>	<algorithm>	<numeric>	<memory>
<utility>	 	 	 

26. STL容器

它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。STL 提供有 3 类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器
两大类:序列容器 关联容器

容器种类 功能
序列容器 主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。
排序容器 包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。
哈希容器 C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。

序列容器

  1. vector : 将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),数组尾部添加或移除元素非常快速O(1)。但是在中部或头部安插元素比较费时; O(n)
  2. List : 双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)),在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针;
  3. Deque : 是double-ended queue的缩写,可以随机存取元素(用索引直接存取),数组头部和尾部添加或移除元素都非常快速。但是在中部安插元素比较费时;
//vector常用操作
 v.capacity();  //容器容量
 v.size();      //容器大小
 v.max_size();  //表示返回vector最大容量
 v[int idx];	//访问元素
 v.at(int idx); //用法和[]运算符相同
 v.push_back(); //尾部插入
 v.pop_back();  //尾部删除
 v.front();     //获取头部元素
 v.back();      //获取尾部元素
 v.begin();     //头元素的迭代器
 v.end();       //尾部元素的迭代器
 v.insert(pos,elem); //pos是vector的插入元素的位置
 v.insert(pos, n, elem) //在位置pos上插入n个元素elem
 v.insert(pos, begin, end);
 v.erase(pos);   //移除pos位置上的元素,返回下一个数据的位置
 v.erase(begin, end); //移除[begin, end)区间的数据,返回下一个元素的位置
 v.reverse(pos1, pos2); //将vector中的pos1~pos2的元素逆序存储
//list容器常用操作
push_back()  //在list的末尾添加一个元素 
push_front() //在list的头部添加一个元素 	 
pop_back() 	 //删除最后一个元素 
pop_front()  //删除第一个元素 
sort() 		 //给list排序 
reverse() 	 //把list的元素倒转 

关联容器:
map容器是键-值对的集合,map中的所有元素都是pair,可以使用键作为下标来获取一个值。Map中所有元素都会根据元素的值自动被排序,同时拥有实值value和键值key,pair的第一元素被视为键值,第二元素被视为实值,同时map不允许两个元素有相同的键值。

//map容器常用操作
map<string, int> m;
pair<string, int> p("hehe", 10);
//p.first = key
//p.second = value
m.insert(p) ;
//如果键p.first不在m中,则插入一个值为p.second的新元素;如果该键在m中已存在,那么不进行任何操作。 
//用数组方式插入:
m["str"] = 1;
//如果"str"不在m中,则插入一个值为1的新元素;如果该键在m中已存在,则修改值为1;

map<string, int>::iterator iter = m.find("小红");
if(iter != m.end())
	cout<<iter->first<<" "<<iter->second<<endl;
else
	cout<<"查无此项"<<endl;

//删除操作	
//1.通过迭代器指针
iterator erase(iterator it);//通过一个条目对象删除
iterator erase(iterator first,iterator last)//删除一个范围
//2,通过键
m.erase("aaa");

用下标访问map中不存在的元素将导致在map容器中添加一个新的元素,这个元素的键即为该下标值,键所对应的值为空。

27. 迭代器

迭代器是一种检查容器内元素并遍历元素的数据类型。C++更趋向于使用迭代器而不是下标操作,因为标准库为每一种标准容器(如vector)定义了一种迭代器类型,而只用少数容器(如vector)支持下标操作访问容器元素。
C++中的迭代器是对指针的封装,迭代器提供特定的函数让调用者对指针进行特定的安全的操作。

//vector容器
vector<int> v;
vector<int>::iterator iter;
//begin是容器的头元素 ,end并不是尾元素,而是无效的元素表示容器结束
for(iter = v.begin(); iter != v.end(); iter++)
{
      cout<<*iter<<" ";
}

//map容器
map<string, int> m;
for(map<string, int>::iterator iter = m.begin();iter != m.end();iter++)
{
	cout<<iter->first<<" "<<iter->second<<endl;
}
//删除
iterator erase();

28.类的类型转换与explicit

《C++ Primer》中提到:
“可以用单个形参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。”
这里应该注意的是, “可以用单个形参进行调用” 并不是指构造函数只能有一个形参,而是它可以有多个形参,但那些形参都是有默认实参的。
那么,什么是“隐式转换”呢? 上面这句话也说了,是从构造函数形参类型到该类类型 的一个编译器的自动转换。看一段代码实例:

class CxString  
{  
public:  
    char *_pstr;  
    int _size;  
    // 没有使用explicit关键字的类声明, 即默认为隐式声明  
    CxString(int size)  
    {  
        _size = size;                // string的预设大小  
        _pstr = malloc(size + 1);    // 分配string的内存  
        memset(_pstr, 0, size + 1);  
    }  
    CxString(const char *p)  
    {  
        int size = strlen(p);  
        _pstr = malloc(size + 1);    // 分配string的内存  
        strcpy(_pstr, p);            // 复制字符串  
        _size = strlen(_pstr);  
    }  
    // 析构函数这里不讨论, 省略...  
};   
    // 下面是调用:    
    CxString string1(24);     // 这样是OK的, 为CxString预分配24字节的大小的内存  
    CxString string2 = 10;    // 这样是OK的, 为CxString预分配10字节的大小的内存  
    CxString string3;         // 这样是不行的, 因为没有默认构造函数, 错误为: “CxString”: 没有合适的默认构造函数可用  
    CxString string4("aaaa"); // 这样是OK的  
    CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)  
    CxString string6 = 'c';   // 这样也是OK的, 其实调用的是CxString(int size), 且size等于'c'的ascii码  
    string1 = 2;              // 这样也是OK的, 为CxString预分配2字节的大小的内存  
    string2 = 3;              // 这样也是OK的, 为CxString预分配3字节的大小的内存  
    string3 = string1;        // 这样也是OK的, 至少编译是没问题的, 但是如果析构函数里用free释放_pstr内存指针的时候可能会报错, 完整的代码必须重载运算符"=", 并在其中处理内存释放

上面的代码中, “CxString string2 = 10;” 这句为什么是可以的呢? 在C++中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象. 也就是说 “CxString string2 = 10;” 这段代码, 编译器自动将整型转换为CxString类对象, 实际上等同于下面的操作:

CxString string2(10);  
或  
CxString temp(10);  
CxString string2 = temp;  

但是, 上面的代码中的_size代表的是字符串内存分配的大小, 那么调用的第二句 “CxString string2 = 10;” 和第六句 “CxString string6 = ‘c’;” 就显得不伦不类, 而且容易让人疑惑. 有什么办法阻止这种用法呢? 答案就是使用explicit关键字. 我们把上面的代码修改一下, 如下:

class CxString 
{  
public:  
    char *_pstr;  
    int _size;  
     // 使用关键字explicit的类声明, 显示转换  
    explicit CxString(int size)  
    {  
        _size = size;  
        // 代码同上, 省略...  
    }  
    CxString(const char *p)  
    {  
        // 代码同上, 省略...  
    }  
};  
    // 下面是调用:  
    CxString string1(24);     // 这样是OK的  
    CxString string2 = 10;    // 这样是不行的, 因为explicit关键字取消了隐式转换  
    CxString string3;         // 这样是不行的, 因为没有默认构造函数  
    CxString string4("aaaa"); // 这样是OK的  
    CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)  
    CxString string6 = 'c';   // 这样是不行的, 其实调用的是CxString(int size), 且size等于'c'的ascii码, 但explicit关键字取消了隐式转换  
    string1 = 2;              // 这样也是不行的, 因为取消了隐式转换  
    string2 = 3;              // 这样也是不行的, 因为取消了隐式转换  
    string3 = string1;        // 这样也是不行的, 因为取消了隐式转换, 除非类实现操作符"="的重载

explicit关键字的作用就是防止类构造函数的隐式自动转换.

上面也已经说过了, explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了.
但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数。

参考文章:
https://baifeng.blog.csdn.net/article/details/117563488?spm=1001.2014.3001.5506

你可能感兴趣的