AkiraZheng's Time.

C++基础知识学习

Word count: 12.8kReading time: 51 min
2024/03/09

一、指针

1.1 C++的双指针

C++的双指针是指一个指针指向另一个指针的指针,如int **p,其指向的是一个指针的地址,也就是说,p=某个指针的地址*p=这个指针指向的地址,而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
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
#include <iostream>
using namespace std;

int aa = 4;
void singlePointer(int *p1){
cout << "-----singlePointer-----" << endl;
cout << "&p1=" << &p1 << " 函数中的p是重新创建的指针" << endl;
cout << "p1=" << p1 << " 传进来的是原先a的地址" << endl;
*p1 = 5;
cout << "-----singlePointer END-----" << endl;
}


void doublePointer(int **p2){
cout << "-----doublePointer-----" << endl;
cout << "&p2=" << &p2 << " 函数中的p是重新创建的指针" << endl;
cout << "p2=" << p2 << " 传进来的是原先p的地址" << endl;
*p2 = &aa;
cout << "-----doublePointer-----END" << endl;
}

int main()
{
int a = 100;
int *p = &a;
cout << "&a=" << &a << " | "<< "&p="<< &p << " | " << endl;
cout << "p=" << p << " | " << "*p=" << *p << endl;

singlePointer(p);
cout << "&a=" << &a << endl;
cout << "a=" << a << endl;
doublePointer(&p);
cout << "p=" << p << endl;
cout << "*p=" << *p << endl;

return 0;
}
  • 运行结果:

  • 原因:
    • p本身也是一个int型变量,只是存的值是地址;对所存地址进行*p解引用就可以实现取值;
    • 单指针函数的话传的是p,所以其实传进去的是a的地址,然后该地址赋值给新的指针p1,所以已经没有原先p指针的信息了,无法实现修改原先p指针的指向对象,只能改变a的值;
    • 双指针传的是&p,也就是p的地址,所以该地址赋值给新的指针p2,可以实现修改原先p的指向对象

1.2 指针和引用的区别

  • 指针是一个变量,它存储的是一个地址;而引用是一个别名,它是一个常量,它本质上跟被引用的变量是同一个东西。
  • 指针有多级而引用只有一级;指针可以指向空,而引用不行;通过指针传参时会重新拷贝到另一个指针中,而引用会直接传实参
  • sizeof时,指针占用的空间是固定的(8bytes),而引用占用的空间是被引用变量的大小;

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

  • 指针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(资源获取即初始化,智能指针就是采用的RAII实现的资源管理)技术,即在构造函数中申请资源,在析构函数中释放资源,这样可以保证资源的及时释放。
  • 用工具检查内存泄漏,如BoundsCheckerValgrind
  • 调用DEBUG版程序的CRT堆栈提示分析泄漏原因

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

1.4.3 智能指针

智能指针是C++11引入的一种内存管理方式,它是一个类模板,可以自动管理内存,避免内存泄漏。智能指针在<memory>头文件中。

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

常用的智能指针有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循环引用问题。它不控制对象的生命周期,但是可以判断对象是否存在。

由于weak_ptr只做引用不做计数,所以当指向的对象被释放时,weak_ptr是不知道的,因此需要用lock函数将weak_ptr提升为shared_ptr,这样就可以判断对象是否存在。

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

shared_ptr<TestSmartPtr> p1(new TestSmartPtr("Test"));
weak_ptr<TestSmartPtr> p2 = p1;

if (p2.lock()) {
cout << "对象还存在" << endl;
}
else {
cout << "对象已经过期" << endl;
}

return 0;
}

当两个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指针(所以成员函数的作用周期就是this的作用周期)

由于成员函数是在代码区的,所以this指针隐式传入成员函数的作用就是让成员函数直到是那个实例化对象在调用该函数。所以delete this的话会释放掉该实例化对象的内存空间。而由于delete this本身就是释放内存会调用析构函数,所以如果在析构函数中调用delete this会导致无限递归调用析构,最终堆栈溢出从而导致程序崩溃

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

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

this指针存在或者全局区或者寄存器中。标准情况下this为右值,不能通过取地址符号返回this指针的地址,但是可以返回this指针指向的地址。

1.6 new和delete

1.6.1 实现机制

new和delete是C++中用来动态分配释放内存的运算符。

  • new:首先执行标准库函数operator new分配内存 -> 然后调用构造函数初始化对象 -> 最后返回对象的指针(如果内存分配失败,会抛出std::bad_alloc异常)
  • delete:首先调用析构函数 -> 然后执行标准库函数operator delete释放内存

1.6.2 new和malloc的区别

new底层是通过malloc实现的,所以理论上是可以直接free掉new出来的实例化对象,但是这样的话不会调用实例化对象的析构函数

  • new是C++运算符,malloc是C标准库函数
  • 内存分配:new是自动计算大小的,malloc是需要手动输入分配的内存大小;new使用operator new分配内存
  • 内存分配失败:new分配失败时会抛出异常报错,而malloc会直接返回空指针
  • 初始化和释放:new初始化会调用构造函数、delete时会调用析构函数再释放,而malloc会直接释放内存
  • 数据类型检查:new会有数据类型检查,而malloc直接申请一块内存,需要做类型强制转换,也不进行类型安全性检查

1.7 辨别几种指针的区别

1
2
3
4
int *p[10];
int (*p)[10];
int * p(int);
int (*p)(int);
  • 指针数组:存放多个指针的数组int *p[10]
  • 数组指针:某个普通数组的指针,如int (*p)[10]表示一个int型数组的地址
  • 函数返回指针int * p(int)表示函数返回一个int型的指针,传参为int
  • 函数指针int (*p)(int)其中p表示某个函数的指针,返回值是int,传参是int

二、多态

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

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

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

2.1 动态多态:

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

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

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

2.1.1 虚函数

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

  • 虚函数表:每个带有虚函数的都有一个虚函数表
  • 虚函数指针:每个实例化对象都有一个虚函数8bytes的指针,指向虚函数表
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,因为对象的大小至少要为1)。

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

    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

2.4 C++面向对象三大特性:封装、继承、多态

2.4.1 封装

封装是指将数据操作数据的函数封装在一起,形成一个。封装可以隐藏类的实现细节,只提供公共接口给外部使用。

2.4.2 继承

继承是让子类继承父类的属性和方法,修饰符publicprotectedprivate用来控制继承的访问权限。

  • public:公有继承,子类可以访问父类的public成员
  • protected:保护继承,子类可以访问父类的protected+public成员
    • 但是再次继承该子类的类不能访问父类的protected成员
    • 其实例化对象也不能访问父类的protected成员
  • private:私有继承,子类可以访问父类的private+protected+public成员

2.4.3 多态

多态如上面说的分为静态多态动态多态,其中动态多态是通过虚函数实现的。

  • 静态多态是通过函数重载运算符重载实现的,是编译时的多态
  • 动态多态是通过虚函数实现的,是运行时的多态
    • 基类指针或者引用绑定派生类对象时,调用虚函数时,会调用派生类的虚函数

三、虚基类

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

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

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

参考:C++ 虚基类

四、深拷贝和浅拷贝

4.1 浅拷贝

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

移动构造函数通常就是浅层拷贝,传参是右值引用(将亡值)。我们用对象a去实例化b时,实例化完后a其实就没用了,所以我们可以将a的资源直接转移到b,这样就不用再重新分配内存。因此传入右值或者将亡值时才会触发移动构造函数

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新特性,可以自动推导变量的类型,可以用于变量声明函数返回值模板参数等。

auto是编译器通过初始值去判断类型的

6.2 decltype

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

decltype是编译器通过变量或表达式去判断类型的

使用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;
    }

七、C++11新特性

  • auto:自动类型推导
  • decltype:获取表达式的类型
  • lambda:是一种内嵌的匿名函数(闭包实例)
    • 可以捕获变量(值分为捕获和引用捕获两种)、传递参数、返回值等
  • nullptr:用来代替NULLNULL在C++中是一个宏定义,跟0是一样的,所以在重载函数时会出现二义性问题,而nullptr是一个关键字
  • 智能指针shared_ptrunique_ptrweak_ptr
  • 初始化列表{},用来初始化数组、结构体、类等,其中list列表变量初始化的顺序是由参数声明的顺序决定的,不是初始化列表的顺序决定的
  • 基于范围的for循环:for(auto i : v)
  • 右值引用&&,用来实现移动语义
    • 右值(将亡值/纯右值)不能取地址,左值可以取地址
    • 右值引用不能绑定到任何左值,如果想实现绑定,需要使用std::move函数
    • 左值引用就是普通的引用
      1
      2
      3
      4
      5
      int a = 10;//左值是a,右值是10
      int &b = a;
      //int &c = 10;//编译报错,10是右值
      int &&c = 10;
      c = 20;//修改右值
  • noexcept:用来指定函数是否抛出异常
  • explicit:用来修饰构造函数,取消隐式转换

九、C++ STL

C++中STL是指:

  • 算法:排序、查找、替换等方法
  • 容器:vector、list、deque、set、map等数据结构的容器,可以容纳不同类型的数据
  • 迭代器:指针用来遍历容器的数据

顺序式容器有:vectorlistdeque(双向开口线性空间)、queuestackpriority_queue

  • 除了list外,插入删除(insert/erase)都会使原先取出的迭代器失效

关联式容器有:setmapmultisetmultimap

  • 插入删除不会使迭代器失效(只有删除的那个元素的迭代器失效)
  • set和map是红黑树实现的,所以查找、插入、删除的时间复杂度都是O(logn)

各种容器的迭代器类别如下表:

容器 迭代器类别 说明
vector deque 随机访问迭代器 支持+-+=-=[]<><=>=
stack queue priority_queue 不支持迭代器(不能被遍历) 无法遍历
list (multi)map/set 双向迭代器 支持++--*->
unordered_map/set forward_list 前向迭代器(只能向前移动) 支持++*->

9.1 STL动态分配空间

从堆上分配空间的话有一级配置器二级配置器

  • 一级配置器是通过mallocfree实现的
  • 二级配置器是默认的分配方式(需要分配的空间小于128Byte时使用的),是通过维护内存池+自由链表实现的

9.2 vector

1)基础知识

vector是分配的连续地址空间,所以假设我们提前获取了vector中某个元素的地址,然后再插入元素,由于要保证vector的连续性,所以这个地址就会失效

  • deque是由一段一段连续的空间拼接起来的

2)vector的双倍扩容

vector中有效元素个数是size(),而分配的空间大小是capacity(),当插入元素且有size()==capacity()时,说明vector中的空间已经分配完了,这时就需要重新分配空间

  • dequeue、stack、queue也需要提前分配内存,但是是一次分配固定大小的内存,且不用重新拷贝,而是通过指针指向下一段的内存。

vector当分配的空间不足时,新分配的空间会是原来的2倍,因为每次扩容就需要拷贝原先的数据,所以多分配内存的话能够减少内存分配和拷贝的次数,提高效率。

扩容有两种方法:

  • resvser(n):预先分配空间,但是不会改变size(),只会改变capacity(),将capacity()预先扩大到n
  • resize(n,t):同时改变size()capacity()都扩大到n,同时将新增的元素初始化为t

3)vector释放内存

vector有两种方式清楚数组:

  • clear():清空数组,但是不会释放内存,size()变为0,capacity()不变
  • swap(vector<int>()):清空数组,释放内存,size()capacity()都变为0

9.3 list

list双向链表实现的,所以各元素之间地址空间不连续,因此可以在任意位置插入元素,不会影响其他元素的地址。(这与vector不同)

vector和list的区别:

  • vector随机访问效率更高(可以通过下标访问),而list只能通过迭代器访问O(n)
  • list在中间插入时效率高,因为vector插入时需要挪动后面的元素,且插入后后面的地址失效,而list依然有效

9.4 Hash(unordered_map)

unordered_map是C++11中引入的新容器,是哈希表,是一种关联式容器,底层是哈希表实现的,key具有唯一性无序。哈希表查询时间复杂度是O(1)

其中解决哈希冲突的方式是开链法,C++中哈希表中的哈希槽bucket是用vector实现的,但是每个bucket中存储的是链表,因此unordered_map插入新元素时原先的迭代器指针也不会失效。当然也可以使用其他方法解决哈希冲突问题:

  • 线性探测
  • 再散列:维护多个不同的哈希函数,当发生冲突时,再使用另一个哈希函数,直到找到空槽

九、C++、C、Python的区别

1. C++和C的区别

  • 编译链接不同:C++因为有重载,所以函数名会在C++编译器中进行名称改编,而C语言不会。C++编译后会生成.obj文件,C语言编译后会生成.o文件
  • C++支持面向对象,而C语言不支持
  • C++中内存分配和释放是通过newdelete,而C语言只能通过mallocfree
  • C++中有引用,而C语言没有
  • C++中有try/catch/throw的异常处理
  • C++中有模板,而C语言没有
  • C++新增了关键字:namespace、bool、true、false、new等

2. C++和Python的区别

  • C++是编译型语言,Python是解释型语言
  • Python支持的库很多,但是运行速度比C++慢
  • Python有严格的缩进规则,用缩进代表代码块,而C++用花括号{}代表代码块
  • C++变量需要声明,而Python不需要
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指针
    6. 1.6 new和delete
      1. 1.6.1 实现机制
      2. 1.6.2 new和malloc的区别
    7. 1.7 辨别几种指针的区别
  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 模板
    4. 2.4 C++面向对象三大特性:封装、继承、多态
      1. 2.4.1 封装
      2. 2.4.2 继承
      3. 2.4.3 多态
  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
  7. 七、C++11新特性
  8. 九、C++ STL
    1. 9.1 STL动态分配空间
    2. 9.2 vector
    3. 9.3 list
    4. 9.4 Hash(unordered_map)
  9. 九、C++、C、Python的区别
    1. 1. C++和C的区别
    2. 2. C++和Python的区别