AkiraZheng's Time.

C++基础知识学习

Word count: 9kReading time: 37 min
2024/03/09

一、指针

1.1 C++的双指针

C++的双指针是指一个指针指向另一个指针的指针,如int **p,其指向的是一个指针的地址,而这个指针指向的是一个int类型的数据

简单的例子如下:

int a = 10;

int *p = &a; //p指向a的地址&a, *p指向a的值,这是一级指针,它的值是内存中存放变量a的地址

int **pp = &p; //pp指向p的地址&p*pp指向p的值,即&a,这是二级指针,它的值是内存中存放变量p的地址

1.2 指针和引用的区别

指针是一个变量,它存储的是一个地址

引用是一个别名,它是一个常量,它的值是变量的值

指针和引用的使用方式不同:

  • 指针int *p = &a;
    • 通过赋值变量a的地址给指针p
    • 通过*p解引用访问a的值
    • 指针常量的话,指针变量可以重新赋值改变指向的地址,即p = &b;
  • 引用int &r = a;
    • 通过&符号来取地址,然后赋值给引用r
    • 直接通过r来访问a的值
    • 引用不可以重新赋值,即r = b;是不允许的,所以引用必须在定义时初始化

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test {
public:
void printTest(string s) {
cout << "This is : " << s << endl;
}
};

int main() {

Test m_test;

//指针
Test* m_ptr = &m_test;
m_ptr->printTest("指针");
//引用
Test& m_quote = m_test;
m_quote.printTest("引用");


cout << "调试用" << endl;
return 0;
}

1.3 野指针和悬空指针

野指针

指尚未初始化的指针,既不指向合法的内存空间,也没有使用 NULL/nullptr 初始化指针。

出现野指针举例:

1
2
3
int *p;//未初始化,野指针
int *p = NULL;//初始化为NULL,不是野指针
int *p = nullptr;//初始化为nullptr,p不再是不是野指针

悬空指针

指向已经释放的内存地址的指针(释放前是合法的指针)

出现悬空指针的原因主要有三种:

  • 指针释放资源后没有被重新赋值 OR 指针释放后没有置为nullptr
    1
    2
    3
    int *p = new int(10);
    delete p;
    //p没有被重新赋值或者置为nullptr
  • 超出变量作用域
    1
    2
    3
    4
    5
    6
    int *p;
    {
    int a = 10;
    p = &a;
    }
    //a是局部变量,超出作用域,p成为悬空指针
  • 指向函数返回的局部变量的指针或者引用
    1
    2
    3
    4
    5
    int* fun() {
    int a = 10;
    return &a;
    }
    int *p = fun();
    1
    2
    3
    4
    5
    int& fun() {
    int a = 10;
    return a;
    }
    int &p = fun();

上述提到的野指针悬空指针都是危险的,因为它们可能会访问到非法内存,导致程序崩溃。

1.4 智能指针

1.4.1 内存溢出 Out of Memory

内存溢出是指程序申请的内存超过了系统能提供的内存,导致程序崩溃

内存溢出的原因主要是因为申请内存过大:程序申请的内存超过了系统能提供的内存

  • 比如,程序申请了一个很大的数组,但是系统内存不足,导致内存溢出
  • 再比如,申请了一个int变量,但是实际给它赋值了一个很大的数(如long才能存下的数),导致内存溢出
  • 内存泄漏最终会导致内存溢出

1.4.2 内存泄漏 Memory Leak

内存泄漏是指程序分配了一块内存空间,但由于某种原因程序没有释放或者无法释放这块内存空间,导致这块内存空间永远无法被使用,这就是内存泄漏。

内存泄漏的原因主要有两种:

  • 堆内存泄漏:程序在堆上分配了内存,但是没有释放,导致内存泄漏。通常是因为程序员使用new或者malloc分配内存,但是忘记使用delete或者free释放内存
  • 资源泄漏:程序在使用资源时,没有释放,导致资源泄漏。比如打开文件打开数据库连接等,但是没有关闭,久而久之会导致其它程序无法使用它。

如何避免内存泄漏

  • 首先是记得及时释放,一般会在析构函数中释放内存类的资源(但是如果类的对象也是用new分配的内存,那么还是要手动释放对象才能调用析构函数)
  • 其次是使用智能指针,实现自动管理内存。(智能指针过期后会自动调用析构函数,释放内存)
  • 可以使用RAII(资源获取即初始化)技术,即在构造函数中申请资源,在析构函数中释放资源,这样可以保证资源的及时释放。
  • 用工具检查内存泄漏,如BoundsChecker

题外话说一下缓冲区溢出:缓冲区溢出是指如vectorstring等这种带索引的容器,当索引超出容器的范围时,会导致程序崩溃

1.4.3 智能指针

智能指针是C++11引入的一种内存管理方式,它是一个类模板,可以自动管理内存,避免内存泄漏

智能指针的主要作用是解放程序员,实现自动释放内存,当智能指针超出作用域时,会自动调用析构函数,释放内存。

常用的智能指针有unique_ptrshared_ptrweak_ptr是C++11标准,而auto_ptr是C++98标准,已经被C++17废弃。

1.4.3.1 unique_ptr

通常一块内存可以被多个普通指针指向,但是unique_ptr独占的,即一块内存只能有一个unique_ptr指向它

首先来产生用unique_ptr管理普通的指针,实现没有delete也能自动释放普通指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestSmartPtr {
public:
TestSmartPtr(string m_name) :m_name(m_name) {
cout << "调用了" << m_name << "的构造函数" << endl;
}
~TestSmartPtr() {
cout << "调用了" << m_name << "的析构函数" << endl;
}
TestSmartPtr(const TestSmartPtr& other):m_name(other.m_name){
cout << "调用了" << m_name << "的拷贝构造函数" << endl;
}
private:
string m_name;
};

int main() {

//使用智能指针管理普通指针:实现程序结束自动调用析构函数
TestSmartPtr* p = new TestSmartPtr("Test");
unique_ptr<TestSmartPtr> uni_ptr(p);

return 0;
}

结果:

1
2
调用了Test的构造函数
调用了Test的析构函数

但实际上,有了智能指针之后,我们很少再使用普通指针了,所以我们一般是直接在构造函数中new一个对象,或者使用make_unique函数来创建对象。因此初始化时主要有三种方法:

  • unique_ptr<TestSmartPtr> uni_ptr(new TestSmartPtr("Test"));
  • unique_ptr<TestSmartPtr> uni_ptr = make_unique<TestSmartPtr>("Test");
    • auto uni_ptr = make_unique<TestSmartPtr>("Test");
  • unique_ptr<TestSmartPtr> uni_ptr(p);不建议使用这种方式,会暴露原始指针

unique_ptr怎么保证独占?

unique_ptr是通过在其模板类的定义中禁止拷贝构造函数赋值运算符来保证独占的。

  • 禁止拷贝构造函数:unique_ptr(const unique_ptr&) = delete;

    1
    2
    3
    unique_ptr<TestSmartPtr> uni_ptr1(new TestSmartPtr("Test1"));
    unique_ptr<TestSmartPtr> uni_ptr2 = uni_ptr1; //编译报错
    unique_ptr<TestSmartPtr> uni_ptr3(uni_ptr1); //编译报错
  • 禁止赋值运算符:unique_ptr& operator=(const unique_ptr&) = delete;

    1
    2
    3
    unique_ptr<TestSmartPtr> uni_ptr1(new TestSmartPtr("Test1"));
    unique_ptr<TestSmartPtr> uni_ptr2(new TestSmartPtr("Test2"));
    uni_ptr2 = uni_ptr1; //编译报错

为什么需要保证unique_ptr的独占?

如果不独占的话,当我们用多个智能指针指向同一个对象时,当多个智能指针过期时会调用多次析构函数,这样除了第一次调用析构函数是正常的,其它的调用都是对野指针的操作,会导致程序崩溃。

1.4.3.2 shared_ptr

shared_ptr共享的智能指针,它可以多个shared_ptr指向(关联)同一个对象,在内部采用引用计数来实现共享管理:

  • 当有一个shared_ptr与对象关联时,引用计数加1
  • 当最后一个shared_ptr超出作用域时,表示没有任何与其关联的对象了,才会调用析构函数(其它情况的超出作用域只会将引用计数值减1)。此时shared_ptr自动变为nullptr,防止出现悬空指针。

相比于unique_ptrshared_ptr拷贝构造函数赋值运算符允许的,同时还多了一个use_count函数,用来获取当前引用计数的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class TestSmartPtr {
public:
TestSmartPtr(string m_name) :m_name(m_name) {
cout << "调用了" << m_name << "的构造函数" << endl;
}
~TestSmartPtr() {
cout << "调用了" << m_name << "的析构函数" << endl;
}
TestSmartPtr(const TestSmartPtr& other):m_name(other.m_name){
cout << "调用了" << m_name << "的拷贝构造函数" << endl;
}
string getName() {
return m_name;
}
private:
string m_name;
};

int main() {

shared_ptr<TestSmartPtr> p0 = make_shared<TestSmartPtr>("Test");
cout << "p0初始引用计数值:" << p0.use_count() << endl;

//采用拷贝构造函数增加p1对对象的引用
shared_ptr<TestSmartPtr> p1(p0);

cout << "p0当前的引用计数值:" << p0.use_count() << endl;
cout << "p0的user_name:" << p0->getName() << endl;
cout << "p1初始引用计数值:" << p0.use_count() << endl;
cout << "p1的user_name:" << p1->getName() << endl;

return 0;
}

运行结果:

1
2
3
4
5
6
7
调用了Test的构造函数
p0初始引用计数值:1
p0当前的引用计数值:2
p0的user_name:Test
p1初始引用计数值:2
p1的user_name:Test
调用了Test的析构函数

在使用左右值引用时,左值的引用计数会减1右值的引用计数会加1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {

shared_ptr<TestSmartPtr> p0 = make_shared<TestSmartPtr>("Test");
shared_ptr<TestSmartPtr> p1 = make_shared<TestSmartPtr>("Test2");

//采用赋值方法
shared_ptr<TestSmartPtr> p2 = p0;
cout << "p0当前的引用计数值:" << p0.use_count() << endl;
cout << "p1当前的引用计数值:" << p1.use_count() << endl;

cout << "---修改p3的引用赋值对象---" << endl;
p2 = p1;//左值:原始p2指向的p0计数值减一;右值:当前指向的p1引用计数值加一
cout << "p0当前的引用计数值:" << p0.use_count() << endl;
cout << "p1初始引用计数值:" << p1.use_count() << endl;

return 0;
}

运行结果:

1
2
3
4
5
6
7
8
9
调用了Test的构造函数
调用了Test2的构造函数
p0当前的引用计数值:2
p1当前的引用计数值:1
---修改p3的引用赋值对象---
p0当前的引用计数值:1
p1初始引用计数值:2
调用了Test2的析构函数
调用了Test的析构函数

用unique_ptr好,还是shared_ptr好?

一般情况下,能用unique_ptr就用unique_ptr,因为unique_ptr效率更高,而且更安全

而如果有需要共享的情况,那么就用shared_ptr

给unique_ptr和shared_ptr自定义删除器

三种自定义删除器的方式:普通函数、仿函数、lambda表达式

  • 普通函数

    1
    2
    3
    4
    void deleteFunc(TestSmartPtr* t) {
    cout << "使用普通函数方式自定义删除器(全局函数)\n";
    delete t;
    };
  • 仿函数

    1
    2
    3
    4
    5
    6
    struct deleteClass {
    void operator()(TestSmartPtr* t) {
    cout << "使用仿函数的方式自定义删除器\n";
    delete t;
    }
    };
  • lambda表达式

    1
    2
    3
    4
    auto deleteLamb = [](TestSmartPtr* t) {
    cout << "使用Lambda表达式的方式自定义删除器\n";
    delete t;
    };

unique_ptrshared_ptr添加自定义删除器并进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main() {

//给shared_ptr自定义删除器
cout << "------------给shared_ptr自定义删除器---------------" << endl;
shared_ptr<TestSmartPtr> p0(new TestSmartPtr("Test"), deleteFunc);
shared_ptr<TestSmartPtr> p1(new TestSmartPtr("Test2"), deleteClass());
shared_ptr<TestSmartPtr> p3(new TestSmartPtr("Test3"), deleteLamb);

////给unique_ptr自定义删除器
//cout << "------------给unique_ptr自定义删除器---------------" << endl;
//unique_ptr<TestSmartPtr, decltype(deleteFunc)*> p4(new TestSmartPtr("Test decltype"), deleteFunc);
//unique_ptr<TestSmartPtr, void(*)(TestSmartPtr *)> p5(new TestSmartPtr("Test 函数指针"), deleteFunc);
//unique_ptr<TestSmartPtr, deleteClass> p6(new TestSmartPtr("Test 仿函数"), deleteClass());
//unique_ptr<TestSmartPtr, decltype(deleteLamb)> p7(new TestSmartPtr("Test Lambda"), deleteLamb);

return 0;
}
1.4.3.3 weak_ptr

weak_ptrshared_ptr弱引用,是为了解决shared_ptr循环引用问题。它不控制对象的生命周期,但是可以判断对象是否存在。

当两个shared_ptr相互引用时,会导致引用计数永远不为0。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class TestSmartPtr2;
class TestSmartPtr {
public:
TestSmartPtr() {
cout << "调用了" << "第一个类" << "的构造函数" << endl;
}
~TestSmartPtr() {
cout << "调用了" << "第一个类" << "的析构函数" << endl;
}
//weak_ptr<TestSmartPtr2> m_p;//引用TestSmartPtr2
shared_ptr<TestSmartPtr2> m_p;//引用TestSmartPtr2
};

class TestSmartPtr2 {
public:
TestSmartPtr2() {
cout << "调用了" << "第二个类" << "的构造函数" << endl;
}
~TestSmartPtr2() {
cout << "调用了" << "第二个类" << "的析构函数" << endl;
}
//weak_ptr<TestSmartPtr> m_p;//引用TestSmartPtr
shared_ptr<TestSmartPtr> m_p;//引用TestSmartPtr
};

int main() {

shared_ptr<TestSmartPtr> p1(new TestSmartPtr());
shared_ptr<TestSmartPtr2> p2(new TestSmartPtr2());

//进行循环引用
p1->m_p = p2;
p2->m_p = p1;

return 0;
}

运行结果:

1
2
调用了第一个类的构造函数
调用了第二个类的构造函数

解决方法

  • 将两个类中的shared_ptr改为weak_ptr
  • 在需要使用时通过expired函数判断是否过期(线程不安全)
  • weak_ptr不能直接访问资源,但是可以通过lock函数提示为shared_ptr,该函数同时也可以判断是否过期。(线程安全的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int main() {

shared_ptr<TestSmartPtr> p1(new TestSmartPtr());
shared_ptr<TestSmartPtr2> p2(new TestSmartPtr2());

//进行循环引用
p1->m_p = p2;
p2->m_p = p1;

//.lock()将weak_ptr提升为shared_ptr
if (p1->m_p.lock()) {
cout << "对象1还存在,use_count:" << p1.use_count() << endl;
}
else {
cout << "对象1已经过期" << endl;
}

if (p2->m_p.lock()) {
cout << "对象2还存在,use_count:" << p2.use_count() << endl;
}
else {
cout << "对象2已经过期" << endl;
}

return 0;
}

运行结果:

1
2
3
4
5
6
调用了第一个类的构造函数
调用了第二个类的构造函数
对象1还存在,use_count:1
对象2还存在,use_count:1
调用了第二个类的析构函数
调用了第一个类的析构函数

1.5 this指针

this指针是一个隐式的指向当前对象指针this指针只有在对象中的成员函数中才有定义,因此,你获得一个对象后,也不能通过对象使用此指针。(只能在类内部使用,且静态成员函数不能使用)

静态成员函数与一般成员函数的唯一区别就是没有this指针,因此不能访问非静态数据成员

要注意,this指针是常量指针,不能被赋值,也不能被删除。

  • 它会在构造函数析构函数自动创建销毁
  • 如果在析构函数显式删除this指针,由于delete this的操作本身就会调用析构函数,所以会导致无限递归调用析构,最终堆栈溢出从而导致程序崩溃

二、多态

C++的多态有两种:静态多态动态多态

其中,静态多态是通过函数重载运算符重载实现的,而动态多态是通过虚函数实现的。

除此以外,C++还有模板多态,即通过模板实现的多态。(CRTP)

2.1 动态多态:

C++的动态多态是通过父类定义虚函数实现和子类重写来实现。

  • 基类定义至少一个虚函数,此时该基类就会在编译时确定一个静态数组(也就是虚函数表),里面存放了所有虚函数的地址,每个类的所有类对象共享这个虚函数表。
    • 其中,对于普通虚函数,父类可以选择声明实现or不实现虚函数
    • 但是如果是纯虚函数,那么父类不能实现具体函数,只能由子类实现,且此时父类为抽象类,不能实例化。如果子类没有实现纯虚函数,那么子类也是抽象类,不能实例化。

接下来来具体从虚函数虚函数表虚函数指针这几个方面来进行讲解。

2.1.1 虚函数

虚函数是在基类中使用virtual关键字声明的函数。在基类中声明虚函数后,派生类可以覆盖(重写)该函数,实现多态虚函数在内存中存储于代码区,而虚函数表存储于常量区

1
2
3
4
5
6
7
8
9
10
class Base {
public:
virtual void fun1(){
cout << "Base fun1" << endl;
};
virtual void fun2(){
cout << "Base fun2" << endl;
};
virtual ~Base() {}
};

如上述代码,funvirtual关键字声明,那么fun就是一个虚函数。结合下文将说到的虚函数指针没有声明虚函数的类根本不会有虚表指针,也不支持多态(动态)。

2.1.2 虚函数表vtbl

虚函数表中存储了该类所有的虚函数的地址。一个类的所有对象共享一个虚函数表,这个表是在编译时就已经生成的。

如果有一个Base类,其中有一个虚函数fun,那么它的虚函数表中就会存储fun的地址。

Base中的函数 虚函数表地址
fun1 0x00f21569
fun2 0x00f21596
虚析构 0x00f21573

虚函数表是在编译时确定的,且每个类的所有对象共享一个虚函数表。

2.1.3 虚函数指针vptr

虚函数指针是实例化对象中指向虚函数表的地址的指针。含有虚函数的类的对象或者继承了含有虚函数的类的对象都会有一个虚函数指针。

  • 普通类:无虚函数,无虚函数表

    1
    2
    3
    4
    class Base {
    public:
    void fun();
    };

    如上面的代码,Base类中没有虚函数,所以Base类是一个普通类,在编译时不会有虚函数表。

    也因此,当我们创建一个Base类的对象时,该对象自然没有虚函数指针这个变量存在

    所以当我们用sizeof函数查看对象的大小,得到的结果是1(因为对象的大小是由成员变量决定的,而Base类中没有虚函数也没有成员变量,所以对象的大小是1byte,因为对象的大小至少要为0)。

    这也说明了普通函数的地址不存储在类对象中,而是存储在代码段中。

    1
    2
    3
    4
    5
    int main() {
    Base b;
    cout << sizeof(b) << endl; // 1
    return 0;
    }
  • 虚函数类:有虚函数,有虚函数表

    1
    2
    3
    4
    class Base {
    public:
    virtual void fun();
    };

    如上面的代码,Base类中有虚函数,所以Base类是一个虚函数类,在编译时会有虚函数表。当我们创建一个Base类的对象时,用sizeof函数查看对象的大小,得到的结果是4(因为对象的大小是由成员变量决定的,而Base类中有虚函数,所以对象的大小是4byte,因为对象的大小至少要为1)。

    这也说明了虚函数的地址存储在类对象中,而不是存储在代码段中。

    1
    2
    3
    4
    5
    int main() {
    Base b;
    cout << sizeof(b) << endl; // 8(64位系统)
    return 0;
    }

2.1.4 虚函数的工作原理和多态体现

多态原理:

  • 常规的多态是指在基类中定义一个虚函数,然后在派生类重写这个虚函数
  • 这样在基类指针或者引用绑定派生类对象时,调用这个虚函数时,会调用派生类的虚函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Base {
public:
virtual void myvirfunc() {
cout << "Base myvirfunc" << endl;
}
};

class Derived : public Base {
public:
void myvirfunc() {
cout << "Derived myvirfunc" << endl;
}
};

int main() {
Base *p = new Base();
p->myvirfunc(); // 使用指针调用虚函数,属于多态

Base b;
b.myvirfunc(); // 使用普通对象调用虚函数,不属于多态

Base* ybase = &b;
ybase->myvirfunc(); // 使用指针调用虚函数,属于多态
return 0;
}

//下面的情况都属于多态
int main(){
//父类的指针指向子类对象
Base * p = new Derived();
p->myvirfunc();
//or
Derived d;
Base * p2 = &d;
p2->myvirfunc();

//父类引用绑定子类对象
Base & r = d;
r.myvirfunc();
}

内存布局

拥有虚函数的基类会在内存中共享一个虚函数表,而这个虚函数表中存储了所有虚函数的地址。

当我们创建一个基类的对象时,这个对象会有一个虚函数指针,指向这个虚函数表。

虚函数表虚函数指针都是在编译时就已经生成的。

存在继承关系时的内存布局

假设基类Base有三个虚函数fgh,那么在编译时,Base类的虚函数表中会存储这三个虚函数的地址。

当派生类Derived继承Base类时,由于Derived类重写了其中的f函数,所以Derived类的虚函数表中会存储f函数的地址,而gh函数在虚函数表中的值跟Base类一样。

当用Base类的指针指向Derived类的对象时,构造函数会先调用Base类的构造函数,然后调用Derived类的构造函数。由于虚函数指针是跟对象绑定的,所以此时其实用的还是Derived对象的内存空间,所以虚函数指针指向的是子类Derived类的虚函数表

2.1.5 为什么说继承属于动态的多态?

动态多态之所以被称为动态的原因在于,它是在程序运行时(而非编译时)确定对象的类型和应该调用的函数的。

虽然虚函数表虚函数指针都是在编译时确定的(但是并没有分配内存,运行时才会分配内存),但是编译期间虚函数指针是没有具体指向的。

只有当构造函数调用时,虚函数指针才会指向具体的虚函数表,而构造函数的调用是在程序运行时才会发生的,这也是继承属于动态绑定的原因。

总结:编译时查看的是 Shape 类有没有这个接口, 而在运行时会查虚函数表, 才决定具体调用哪个(动态多态)

2.2 静态多态

静态多态是通过函数重载运算符重载实现的。具体不多说了

2.3 模板多态(CRTP)

模板多态是通过模板实现的多态,即CRTP(Curiously Recurring Template Pattern)。(也称为奇异递归模板模式)

CRTP模板继承相结合,形成一种新的设计模式。

(1)通过继承实现的多态是绑定的和动态的

  • 绑定的含义是:对于参与多态行为的类型,它们(具有多态行为)的接口是在公共基类的设计中就预先确定的(有时候也把绑定这个概念称为入侵的或者插入的)。
  • 多态的含义是:接口的绑定是在运行期(动态)完成的。

(2)通过模板实现的多态是非绑定的和静态的

  • 非绑定的含义是:对于参与多态行为的类型,它们的接口是没有预先确定的(有时也称这个概念为非入侵的或者非插入的)。
  • 静态的含义是:接口的绑定是在编译期(静态)完成的。

2.3.1 模板

模板是C++中的一种泛型编程技术,通过模板可以实现类型参数化,即可以将类型作为参数传递给类或者函数。我们常用的vector <int>vector <string>等都是通过模板实现的。C++中编译器会从函数模板通过具体类型产生不同的函数

  • 模板在函数中的使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template <typename T>
    T getMax(T a, T b) {
    return a >= b ? a : b;
    }

    int main() {
    int a = 2;
    int b = 10;
    cout << "getMax(2, 10):" << getMax(a, b) << endl;//getMax(2, 10):10

    double a1 = 44.2;
    double b1 = 10.2;
    cout << "getMax(44.2, 10.2):" << getMax(a1, b1) << endl;//Max(44.2, 10.2):44.2

    return 0;
    }

    从上面的代码例子可以看出,通过模板类的实现使getMax可以接受任意类型的输入参数。

  • 模板在中的使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    template <typename T>
    class testTemp {
    public:
    testTemp(T a1, T b1) :a(a1), b(b1) {}
    T a;
    T b;
    void printMax() {
    cout << "printMax:" << getMax() << endl;
    }

    T getMax();
    };

    template <typename T>
    T testTemp<T>::getMax() {
    return (a >= b ? a : b);
    }

    int main() {
    testTemp<int> m_test(12, 22);
    m_test.printMax();//printMax:22

    testTemp<double> m_test2(12.23, 22.45);
    m_test2.printMax();//printMax:22.45

    return 0;
    }

    2.3.2 CRTP的实现

CRTP是通过模板实现的多态,它的实现原理是通过模板实现继承。也就是创建一个模板类,然后其它类通过继承这个模板类来实现多态。

  • 首先创建一个模板基类
1
2
3
4
5
6
7
template <typename T>
class Base {
public:
void foo() {
static_cast<T*>(this)->printWord("Hello");
}
};
  • 然后创建两个继承这个模板基类的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//通过模板多态的方式基础模板类Base,此时模板传入的参数为类Child1
//Child1类需要有打印的函数printWord
class Child1 : public Base<Child1> {
public:
void printWord(string s) {
cout << "Child1: " << s << endl;
}
};

class Child2 : public Base<Child2> {
public:
void printWord(string s) {
cout << "Child2: " << s << endl;
}
};
  • 接着再提供一个函数,用来调用基类中的foo函数(委托函数printTest作为中间代理)
1
2
3
4
template <typename T>
void printTest(Base<T>& obj) {
obj.foo();
}
  • 最后在main函数中调用
1
2
3
4
5
6
7
8
9
int main() {

Child1 c1;
Child2 c2;
printTest(c1);
printTest(c2);

return 0;
}
  • 运行结果
1
2
Child1: Hello
Child2: Hello

三、虚基类

虚基类是指在多重继承中,子类采用: virtual public Base的方式继承父类,是为了解决二义性问题而引入的

二义性是指在菱形继承中,子类继承了两个父类,而这两个父类又继承了同一个父类,那么子类就会继承两份相同的父类,这就会导致二义性问题。

虚基类的使用可以有效防止菱形继承下调用两次(或多次)父类的构造函数。

参考:C++ 虚基类

四、深拷贝和浅拷贝

4.1 浅拷贝

浅拷贝是指拷贝对象时,只是拷贝对象的值,而对于指针类型变量,不会重新分配内存,而是拷贝指针的地址

4.2 深拷贝

深拷贝相比浅拷贝,对于指针类型变量,会重新分配内存,并将指针指向的地址拷贝到新的内存空间,然后使用memcpy函数将原对象的值拷贝到新的内存空间。

深拷贝可以通过拷贝构造函数赋值运算符重载实现。下面将展示这两种实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class testCopy {
public:
//构造函数创建数组
testCopy(int size, int start) :size_(size) {
data_ = new int[size_];
for (int i = 0; i < size_; i++) {
data_[i] = start + i;
}
}

~testCopy() {
delete[] data_;
}

//拷贝构造函数实现深拷贝
testCopy(const testCopy& copy) {
delete[] data_;
this->size_ = copy.size_;
this->data_ = new int[this->size_];//创建内存空间
memcpy(data_, copy.data_, sizeof(int) * size_);//复制所有数据
}

//运算符重载实现深拷贝
testCopy& operator = (const testCopy & copy){
if (this != &copy) {
delete[] data_;
this->size_ = copy.size_;
this->data_ = new int[this->size_];
memcpy(data_, copy.data_, sizeof(int) * size_);
}
return *this;
}

void printData() {
for (int i = 0; i < size_; i++) {
cout << data_[i] << " ";
}
cout << endl;
}

private:
int size_;
int* data_;
};

int main() {
testCopy t1(4, 7);
testCopy t2(6, 2);
cout << "t1 data:" << endl;
t1.printData();
cout << "t2 data:" << endl;
t2.printData();

//使用拷贝构造函数
cout << "t3 data:" << endl;
testCopy t3(t1);
t3.printData();
//使用运算符重载实现深拷贝
cout << "t3 data:" << endl;
t3 = t2;
t3.printData();

return 0;
}

最终输出结果如下:

1
2
3
4
5
6
7
8
t1 data:
7 8 9 10
t2 data:
2 3 4 5 6 7
t3 data:
7 8 9 10
t3 data:
2 3 4 5 6 7

五、常用的string、vector、map、set

5.1 string VS char*

  • char*是C语言中的字符串,指向一个字符数组,以\0结尾,其内存是由程序员分配和释放的,所以可能会有空间不足的问题。通过const char *str = "hello";的方式创建的字符串是只读的,不能修改。

    1
    2
    const char* str = "hello";
    str[0] = 'H'; //编译报错

    但是通过char str[] = "hello";的方式创建的字符串是可读写的,可以修改。

    1
    2
    char str[] = "hello";
    str[0] = 'H'; //编译通过
  • string是C++中的字符串,是标准库类型STL中的一个类,内部封装了很多字符串操作的方法(比如查找、替换、删除等),而且内存是由系统自动分配和释放的,所以除非内存不足,否则不会出现空间不足的问题。通过string str = "hello";的方式创建的字符串是可读写的,可以修改。

    1
    2
    string str = "hello";
    str[0] = 'H'; //编译通过

1)string和char*的关系

string底层是通过char*实现的,所以string和char*之间可以相互转换。

2)string和char*的相互转换

  • stringchar*stringc_str()函数可以将string转换为char*。通过转换可以在printf等函数中使用string

    1
    2
    3
    string str = "hello";
    printf("%s\n", str.c_str());
    char* cstr = const_cast<char*>(str.c_str());//str.c_str()返回的是const char*,所以需要转换为char*
  • char*stringstring构造函数可以将char*转换为string。以及通过赋值运算符=也可以将char*转换为string

    1
    2
    3
    const char* cstr = "hello";
    string str(cstr);//构造函数
    string str2 = cstr;//赋值运算符

5.2 vector VS 数组

5.2.1 vector的初始化

vector的初始化可以通过构造函数赋值运算符列表初始化的方式进行。

  • 通过构造函数初始化

    1
    2
    vector<int> v0(5);//初始化5个元素,每个元素的值为0
    vector<int> v1(5, 1);//初始化5个元素,每个元素的值为1
  • 通过赋值运算符初始化

    1
    2
    vector<int> v2 = {1, 2, 3, 4, 5};//初始化5个元素,每个元素的值为1, 2, 3, 4, 5
    vector<int> v3 = v2;//将v2的值赋给v3
  • 通过列表初始化初始化

    1
    2
    vector<int> v4{1, 2, 3, 4, 5};//初始化5个元素,每个元素的值为1, 2, 3, 4, 5
    vector<int> v5 = {1, 2, 3, 4, 5};//初始化5个元素,每个元素的值为1, 2, 3, 4, 5
  • 二维vector的初始化

    1
    vector<vector<int>> v6(3, vector<int>(4, 1));//初始化3行4列,每个元素的值为1

5.2.2 vector的迭代器遍历

vector的迭代器遍历可以通过迭代器auto关键字范围for循环的方式进行。

  • 通过迭代器遍历:迭代器是一种指针,可以通过begin()end()函数获取vector首地址尾地址

    1
    2
    3
    4
    5
    6
    7
    8
    vector<int> v = {1, 2, 3, 4, 5};
    for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
    cout << *it << " ";//通过*解引用
    }
    for (auto it = v.begin(); it!=v.end(); i++){
    cout << *it << " ";
    }
    cout << endl;
  • 通过范围for循环遍历

    1
    2
    3
    4
    for (auto i : v) {
    cout << i << " ";
    }
    cout << endl;

5.2.3 vector与数组的区别

  • 相同点
    • vector和数组都是线性结构,都是连续的内存空间
    • vector和数组都是有序的,都可以通过下标访问元素。
  • 不同点
    • vector动态数组,可以动态增加删除元素,而数组是静态数组,长度是固定的。
    • vectorSTL中的容器,提供了很多成员函数,可以方便的进行插入删除查找等操作,而数组没有这些功能。
    • vector,而数组是基本数据类型,所以vector可以继承,而数组不能继承。
    • vector是可以通过v1=v2的方式进行赋值的,而数组不能直接通过这种方式进行赋值。

5.3 set 和 map容器

5.3.1 set

set集合,是一种关联式容器,它的主要特点如下:

  • set底层是红黑树RBTree,是一种平衡二叉树,所以查找插入删除的时间复杂度都是**O(logn)**。
  • 其元素只有key,没有value(或者说value就是key)
  • set中的元素是唯一的,不允许重复。
  • set中的元素是有序的,是按照key升序排列的。
  • set中的元素是不可修改的,如果要修改元素,需要先删除再插入。(否则的话修改会破坏红黑树的平衡性)

set的基本函数操作有:

  • insert:插入元素
  • erase:删除元素
  • find:查找元素
  • count:统计元素个数(因为元素是唯一的,所以相当于判断元素是否存在)
  • begin:返回指向第一个元素的迭代器
  • end:返回指向最后一个元素的迭代器
  • size:返回元素个数
  • empty:判断是否为空

操作举例:

1
2
3
4
5
6
7
8
9
set<int> s;
s.insert(1);
s.insert(2);
if(s.find(1) != s.end()){
s.erase(1);
}
if(s.count(2)){//返回1
cout << "2存在" << endl;
}

5.3.2 map

mapset类似,也是一种关联式容器,底层也是由红黑树RBTree实现的,key具有唯一性有序性。但是map中的元素是pair键值对的形式存在的key唯一的,而value可以重复。

map的基本函数操作有:

  • insert:插入元素
  • erase:删除元素
  • find:查找元素
  • count:统计元素个数(因为元素是唯一的,所以相当于判断元素是否存在)
  • begin:返回指向第一个元素的迭代器
  • end:返回指向最后一个元素的迭代器
  • size:返回元素个数
  • empty:判断是否为空

操作举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <ulitity>
#include <map>

int main() {

map<int, string> m_map;
//三种插入数据的方式:make_pair、pair、类似数组的方式
m_map.insert(make_pair<int, string>(1, "Hello1"));
m_map.insert(pair<int, string>(2, "Hello2"));
m_map[3] = "Hello3";

if (m_map.find(1) != m_map.end()) {
cout << m_map[1] << endl;
}

m_map.erase(2);
if (m_map.count(2) == 0) {
cout << "无2这个key" << endl;
}

return 0;
}

5.3.3 multiset和multimap

扩展讲一下multisetmultimapmultisetmultimapsetmap多重集合版本,特点是允许key重复。底层也是由红黑树RBTree实现的。

六、自动类型推导

自动类型推导有两种方式:autodecltype

6.1 auto

auto是C++11新特性,可以自动推导变量的类型,可以用于变量声明函数返回值模板参数等。

6.2 decltype

decltype是C++11新特性,可以获取表达式的类型,可以用于变量声明函数返回值模板参数等。

使用decltype有两种方式:**decltype(表达式)decltype(变量)**。

  • 变量方法

    1
    2
    3
    4
    5
    6
    int main() {
    int a = 10;
    decltype(a) b = 20;//b的类型和a的类型相同
    auto c = a;//c的类型和a的类型相同
    return 0;
    }
  • 表达式方法

    1
    2
    3
    4
    5
    6
    7
    8
    int main() {
    int a = 10;
    decltype(a) b = 20;//b的类型和a的类型相同
    auto c = a;//c的类型和a的类型相同
    decltype(a + 1) d = 30;//d的类型和a+1的类型相同
    auto e = a + 1;//e的类型和a+1的类型相同
    return 0;
    }
CATALOG
  1. 一、指针
    1. 1.1 C++的双指针
    2. 1.2 指针和引用的区别
    3. 1.3 野指针和悬空指针
    4. 1.4 智能指针
      1. 1.4.1 内存溢出 Out of Memory
      2. 1.4.2 内存泄漏 Memory Leak
      3. 1.4.3 智能指针
        1. 1.4.3.1 unique_ptr
        2. 1.4.3.2 shared_ptr
        3. 1.4.3.3 weak_ptr
    5. 1.5 this指针
  2. 二、多态
    1. 2.1 动态多态:
      1. 2.1.1 虚函数
      2. 2.1.2 虚函数表vtbl
      3. 2.1.3 虚函数指针vptr
      4. 2.1.4 虚函数的工作原理和多态体现
      5. 2.1.5 为什么说继承属于动态的多态?
    2. 2.2 静态多态
    3. 2.3 模板多态(CRTP)
      1. 2.3.1 模板
      2. 2.3.2 CRTP的实现
  3. 三、虚基类
  4. 四、深拷贝和浅拷贝
    1. 4.1 浅拷贝
    2. 4.2 深拷贝
  5. 五、常用的string、vector、map、set
    1. 5.1 string VS char*
    2. 5.2 vector VS 数组
      1. 5.2.1 vector的初始化
      2. 5.2.2 vector的迭代器遍历
      3. 5.2.3 vector与数组的区别
    3. 5.3 set 和 map容器
      1. 5.3.1 set
      2. 5.3.2 map
      3. 5.3.3 multiset和multimap
  6. 六、自动类型推导
    1. 6.1 auto
    2. 6.2 decltype