C++总结

本文章结合许多互联网中的数据。所谓取之互联网,用之互联网。若涉及版权侵犯,可以留言或者私信告知。
目前还有很多知识更新中,有什么错误欢迎评论交流。

1、基本语言

static关键字的作用

  1. 定义全局静态变量

    • 内存位置:静态存储区
    • 生命周期:整个程序运行期间都存在,即程序运行开始到程序结束
    • 初始化:未经初始化的自动为0
    • 作用域:文件作用域(只在声明它的文件内可见
  2. 定义局部静态变量

    • 内存位置:静态存储区
    • 生命周期:整个程序运行期间都存在,即程序运行开始到程序结束
    • 初始化:未经初始化的自动为0
    • 作用域:局部作用域(定义它的函数或者语句块结束时作用域结束,但变量还在内存中,直到该函数再次被调用,值不变)
  3. 定义静态函数

    • 作用域:文件作用域(只在声明它的文件内可见,只可在本cpp使用,不会同其他cpp中的同名函数起冲突)
    • 注意:不要在头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果要多个cpp中复用该函数,就把它的声明提到头文件,否则cpp内部声明需加static修饰。
  4. 定义类的静态成员

    静态成员是类的所有对象共享的成员。

  5. 定义类的静态函数

    在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。如果静态成员函数要引用非静态成员,可以通过对象来引用。(<类名>::<静态成员函数名>(<参数表>))

const的用法

这是一个超链接

  • const修饰的变量,相较于宏定义,可以进行类型检查,节省内存,提高了效率
  • const修饰的函数参数,在函数中,参数值不可改变
  • const修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外,函数参数也除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量

const和static在类中的注意事项

这是一个超链接

static静态成员变量

  • 静态成员变量在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化时不要出现static关键字和public之类的访问规则。
  • 静态成员变量相当于类域中的全局变量,被类的所有对象共享,包括派生类对象
  • 静态成员变量可以作为成员函数的可选参数,普通变量不可以
  • 静态成员变量可以是所属类的类型,而普通数据成员只能声明成类的指针或引用

static静态成员函数

  • 静态成员函数不能调用非静态成员变量和函数,因为静态成员函数无this指针。静态函数可作为类作用域的全局函数
  • 静态成员函数不能声明成虚函数、const、volatile

const成员变量

  • const成员变量只能在类内声明和定义,在构造函数初始化列表中初始化
  • const成员变量只在某个对象的生命周期内是常量,对于整个类而言是可变的,因为类可以创建多个对象,不同对象的const成员变量值是不同的,所以不能在类的声明中初始化const成员变量,因为类对象还没创建,编译器不知道它的值。

const成员函数

  • 不能修改成员变量的值,除非有mutable修饰
  • 只能访问成员变量
  • 不能调用非常量成员函数,以防修改成员变量的值

const static

  • 如果要想成员变量在整个类内都是恒定的常量,应该用类的枚举常量或者static const
  • 在类中进行声明,在类外进行初始化

面向对象和面向过程的区别

  • 面向过程:以过程为中心的编程思想,将解决一个问题的基本步骤列出来,按照执行顺序逐一完成每一个步骤。
  • 面向对象:以对象为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个对象在整个解决问题的步骤中的属性和行为。

面向对象的三大特性

  • 封装:将具体的数据和实现过程封装为类,只能通过接口访问,降低耦合性。

    耦合性:是指一程序中,模块及模块之间信息或参数依赖的程度。

  • 继承:子类继承父类的行为和属性,子类拥有父类中非私有的变量和函数。子类可以重写父类中的方法,增强了耦合性。被final修饰的类不能被继承,被final修饰的成员不能被重写或修改。

  • 多态:不同子类对同一个消息,作出不同的反应。基类的指针指向子类的对象,使得基类指针作出不同的应答。

    多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定函数地址;动态多态是用虚函数机制实现的,在运行期间动态绑定。

如何实现多态

C++的多态分为两类,一个是编译期多态,一个是运行期多态
编译期多态主要是通过模板类来实现的。
运行期的多态主要是通过虚函数结合动态绑定实现的

虚函数如何实现多态?

这是一个超链接

子类重写父类虚函数,虚函数表中,该函数的地址直接替换父类虚函数在虚表中的位置。访问虚函数表,首先取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl,然后就可根据调用的虚函数取出slot存放的函数地址,最后调用这个函数。对象模型的头部一般存放虚函数表的指针,通过该机制实现多态。

只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。

虚函数

在有虚函数的类中,类最前面的位置是一个虚函数表的指针vptr,这个指针指向一个虚函数表,表中存放所有虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承父类,也会继承其虚函数表,当子类重写父类中的虚函数时,会将其继承到的虚函数表中的地址替换为重写的函数地址。每个对象中的vptr都是独立一份的,且在该类对象被构造时被初始化。使用虚函数,会增加访问内存开销,降低效率。

虚函数表在编译期生成,存放在全局数据区,即静态区

虚继承

虚继承是用来解决多重继承中的命名冲突数据冗余问题。比如类C继承类B1、类B2,而类B1和类B2都是继承了A,那么在C中就有两份类A里的数据,虚继承就是,B1和B2对A都是虚继承,那么A就是虚基类。这样在派生类C中就只有一份类A中的数据。

虚继承的目的就是让类声明,愿意共享基类,这个基类就是虚基类

C和C++的区别

  • 设计思想上

    C++面向对象的语言,C面向过程的结构化编程语言

  • 语法上

    • C++具有重载、继承和多态三种特性

    • C++相比C,增加许多类型安全的功能,比如强制类型转换

    • C++支持范式编程,比如模板类、函数模板类等

C++中四种cast转换

  1. const_cast

    用于将const变量转为非const

  2. static_cast

    • 用于各种隐式转换,比如非const转为const,void*转指针等

    • 用于多态向上转化,向下转能成功但是不安全,并且结果未知

      比如short->int,int->double风险较低

  3. dynamic_cast

    用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上(和向下)转换。只能转指针或引用类型。对于指针,如果转换失败将返回NULL,对于引用,如果转换失败将抛出std::bad_cast异常。

    向下转换:指的是子类向基类的转换

    向上转换:指的是基类向子类的转换

    它通过判断执行到该语句时,变量的运行类型和要转换的类型是否相同来判断是否能够进行向下转换。

  4. reinterpret_cast

    仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,几乎什么都可以转,比如将int转指针,所以风险很高。

为什么不使用C的强制转换?

转换不明确,不能进行错误检查,容易出错。

const_cast <new_type>(expression)
static_cast <new_type>(expression)
dynamic_cast <new_type>(expression)
reinterpret_cast <new_type>(expression)
    
const int constant = 21;
int* modifier = const_cast<int*>(&constant);
*modifier = 7;
/*
constant = 21
modifier = 7
指向同一个地址
*/

C/C++中指针和引用的区别

  • 指针有自己的一块空间,引用只是一个别名
  • 指针的大小是4(32位)/8(64位)个字节,引用则是被引用对象的大小
  • 指针可以为空,引用初始化必须绑定对象
  • 作为参数传递时,指针需要被解引用才可以对对象进行操作,引用直接修改便会改变引用指向的对象
  • 有const指针,没有const引用
  • 指针在使用中可以改变指向对象,引用只能是一个对象的引用,不能改变
  • **指针可以有多级指针(p),引用只有一级
  • 指针使用++运算符是地址+1,引用使用++运算符是引用的对象值+1
  • 返回动态内存分配的对象或者内存必须使用指针,引用可能引起内存泄漏

C++的smart pointer四个智能指针

四个智能指针:auto_ptr,shared_ptr,weak_ptr,unique_ptr。后三个是C++11支持的,第一个已经被C++11弃用了。

  • auto_ptr(C++98的方案,C++11已经抛弃)

    采用所有权模式。

    auto_ptr<string>p1 (new string("I reigned lonely as a cloud."));
    auto_ptr<string>p2;
    p2 = p1;//不会报错
    

    p2剥夺了p1的所有权,当程序运行时访问p1将会报错,这就展现出auto_ptr的缺点:存在潜在的内存崩溃问题

  • unique_ptr(替换auto_ptr)

    unique_ptr是独占资源所有权的指针,保证同一时间只有一个智能指针可以指向该对象。对于避免资源泄漏特别有用。(new创建对象后因为发生异常没有调用delete)

    采用所有权模式。

    unique_ptr<string>p3 (new string("I reigned lonely as a cloud."));
    unique_ptr<string>p4;
    p4 = p3;//会报错
    

    认为p4=p3非法,避免p3不再指向有效数据的问题。所以unique_ptr比auto_ptr更安全。

    当程序试图将一个unique_ptr赋值给另一个,如果源unique_ptr是个临时右值,允许;如果源unique_ptr将存在一段时间,禁止。

    unique_ptr<string>pu1 (new string("hello"));
    unique_ptr<string>pu2;
    pu2 = pu1;//#1 not allowed
    unique_ptr<string>pu3;
    pu3 = unique_ptr<string>(new string("world"));//#2 allowed
    

    #1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它会调用unique_ptr的构造函数,该构造函数创建的临时对象在其所有权让给pu3后会被销毁。

    如果想执行#1的操作,要安全的重用这种指针,可以给它赋新值。C++中有一个标准库函数std::move(),能够将一个unique_ptr赋给另一个。

    unique_ptr<string>p1,p2;
    p1 = unique_ptr<string>(new string("hello"));
    p2 = move(p1);//此时p1未赋值,只是转移,没有任何拷贝的意思
    p1 = unique_ptr<string>(new string("world"));
    cout<<*p1<< ' '<<*p2<<'\n';//输出:hello world
    
  • shared_ptr

    shared_ptr是共享资源所有权的指针。多个智能指针可以指向相同对象,该对象和其相关资源会在最后一个引用被销毁时释放。使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr,shared_ptr来构造。当我们调用reset()时,当前指针会释放资源所有权,计数减一。当计数=0时资源会被释放。

    shared_ptr是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。

    成员函数:

    user_count 返回引用计数的个数

    unique 返回是否独占所有权(user_count=1)

    swap 交换两个shared_ptr对象(即交换所拥有的对象)

    reset 放弃内部对象的所有权或拥有对象的变更,会引起原有对象引用计数的减少

    get 返回内部对象(指针),由于已经重载了方法,因此和直接适用对象是一样的,如shared_ptrsp(new int(1));sp与sp.get()是等价的。

    shared_ptr<string>p1(new string("hello"));
    shared_ptr<string>p2(p1);
    cout<<*p1.get()<<'\n';
    cout<<p1.use_count()<<'\n';
    p1.reset();
    cout<<p2.use_count()<<'\n';
    if(p2.unique())cout<<"lonely"<<'\n';
    else cout<<"not lonely"<<'\n';
    /*
    hello
    2
    1
    lonely
    */
    
  • weak_ptr

    weak_ptr是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理是那个强引用的 shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段,可以通过传入shared_ptr或weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。

    成员函数

    weak_ptr没有重载*和->,但可以使用lock获得一个可用的shared_ptr对象(weak_ptr在使用前需要检查合法性)

    expired 用于检测所管理的对象是否已经释放,如果已经释放返回true,否则返回false

    lock 用于获取所管理对象的强引用(shared_ptr),如果对象存在返回以恶搞shared_ptr,其内部对象指向与weak_ptr相同,否则返回一个空的shared_ptr。

    reset weak_ptr置空

    weak_ptr支持拷贝和赋值,但不会影响shared_ptr的计数

    使用 weak_ptr 解决 shared_ptr 因循环引有不能释放资源的问题

    class Person{
    private:
        string name;
        shared_ptr<Person>partner;//weak_ptr<Person>partner
    public:
        Person(const string& _name):name(_name){
            cout<<name<<" created"<<'\n';
        }
        virtual ~Person(){
            cout<<name<<" destroyed"<<'\n';
        }
        friend bool partnerUp(std::shared_ptr<Person>& p1, std::shared_ptr<Person>& p2)
        {
            if (!p1 || !p2)
            {
                return false;
            }
    
            p1->partner = p2;  // weak_ptr重载的赋值运算符中可以接收shared_ptr对象
            p2->partner = p1;
    
            cout << p1->name << " is now partenered with " << p2->name << endl;
            return true;
        }
    };
    int main(){
        auto p1 = std::make_shared<Person>("Lucy");
        auto p2 = std::make_shared<Person>("Ricky");
        partnerUp(p1, p2);  // 互相设为伙伴
    }
    /*
    Lucy created
    Ricky created
    Lucy is now partenered with Ricky
    
    shared_ptr<Person>partner->weak_ptr<Person>partner
    Lucy created
    Ricky created
    Lucy is now partenered with Ricky
    Ricky destroyed
    Lucy destroyed
    */
    

    p1和p2相互引用,两个资源引用计数为2,跳出函数计数都减一,但还是不等于0,资源无法被释放。可以将成员属性改为weak_ptr,这样互相引用不会影响计数,最后跳出函数计数为0,资源可以被释放。

为什么要是有智能指针?

智能指针的作用是管理一个指针。因为可能存在申请的空间函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上避免这种问题,因为智能指针就是一个类,当超出类的作用域,类会自动调用析构函数释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间

智能指针基于RAII(Resource Acquisition Is Initialization),称为"资源获取就是初始化",是一种利用对象生命周期控制程序资源的技术

智能指针有没有内存泄漏的情况?

当两个对象互相使用一个shared_ptr成员指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

如何解决智能指针内存泄漏?

为了解决循环引用引进了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,就类似一个普通指针,但不指向引用计数的共享内存,可以检测所管理的对象是否已经被释放,从而避免非法访问。

数组和指针的区别

指针 数组
保存数据的地址 保存数据
间接访问数据,首先获得指针的内容,然后将其作为地址,从该地址中提取数据 直接访问数据
通常用于动态的数据结构 通常用于固定数目且数据类型相同的元素
通过malloc分配内存,free释放内存 隐式的分配和删除
通常指向匿名数据,操作匿名函数 自身即为数据名

野指针是什么?

野指针就是指向一个已删除的对象或者未申请访问受限内存区域的指针

引用

引用的基本使用

作用:给变量取别名

语法数据类型 &别名 = 原名

注意事项

  • 引用必须初始化
  • 引用在初始化后不可改变

引用的本质

在c++内部实现是一个指针常量

为什么析构函数必须是虚函数?

这是为了防止内存泄漏。如果不定义成虚函数,那么只会调用基类的析构函数,等于只释放了基类的内存,而没有释放掉派生类的内存,定义为虚函数后,会先调用派生类的析构函数,然后调用基类的析构函数。

为什么C++默认的析构函数不是虚函数?

因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。对于不会被继承的类,使用虚函数就是浪费内存。

构造函数为什么不能写成虚函数?

每一个拥有虚函数的类对象都有一个指向虚函数表的指针。对象通过虚函数表里存储的虚函数指针来调用虚函数。而虚函数表指针是在调用构造函数的时候初始化的,等于在调用构造函数的时候,此时虚函数表指针还不存在,找不到对应虚函数指针,所以不能将构造函数定义为虚函数。

构造函数和析构函数为什么不能调用虚函数?

这是一个超链接

语法上没错,但是没有调用的必要

对于构造函数,基类调用了虚函数,此时派生类的虚函数还没构造好,因为派生类肯定比基类晚构造,所以你调用的虚函数就是基类里面的,这样就没有实现多态。

同理,析构的话肯定是派生类先析构,那么此时你基类的析构函数调用虚函数,派生类都被析构完了,调用的也只是基类内的函数而已,也没有实现多态。

设计者可能希望在基类指针指向子类对象时,通过该基类指针调用的虚函数版本应该是子类的虚函数版本。结果不能达到需要的目的。设为普通函数照样可以达到这个效果。

函数指针和指针函数

指针函数

一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。

int *fun(int x,int y);
int * fun(int x,int y);
int* fun(int x,int y);

函数指针

其本质是一个指针变量,该指针指向这个函数。即函数指针就是指向函数的指针。

//函数
int add(int x,int y){
    return x+y;
}
//函数指针
int (*fun)(int x,int y);
int main(){
    fun = add;//or fun = &add
    cout<<fun(1,2)<<'\n';
    cout<<(*fun)(3,4)<<'\n';//更明显看出这是一个指针
    return 0;
}

构造函数和析构函数

对象的初始化和清理是两个非常重要的安全问题

  • 一个对象或者变量没有初始状态,对其使用后果是未知

  • 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供空实现的构造函数和析构函数

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法: ~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

静态函数和虚函数的区别

静态函数在编译时就已经确定运行时机(函数地址),虚函数在运行时动态绑定。虚函数因为用了虚函数表机制,调用时会增加一次内存开销。

重载和重写的区别

重载:同一个作用域下,函数名称相同,函数参数类型个数或者顺序不同

重写:不同类之间,父类与子类,函数返回值类型函数名参数列表完全一致称为重写。父类中被重写的函数需要用virtual修饰。

++i和i++的区别

++i先自增1,再返回i

int& int::operator++(){
    *this += 1;
    return *this;
}

i++先返回i,再自增1

const int int::operator(int){
    int oldValue = *this;
    ++(*this);
    return oldValue;
}

实现在main函数执行前先执行

#include <stdio.h>
__attribute((constructor)) void before_main()  
{  
    printf("%s\n",__FUNCTION__);  
}  
__attribute((destructor)) void after_main()  
{  
    printf("%s\n",__FUNCTION__);  
}  
int main( int argc, char ** argv )  
{  
    printf("%s\n",__FUNCTION__);  
    return 0;  
}  
/*
before_main
main
after_main
*/

C++是怎么定义常量的?常量存放在内存哪个位置?

使用define预处理器或者const关键字定义常量

#define CLK_TCK 10
const int CLK_TCK = 10;

常量定义必须初始化。对于局部对象,常量存放在栈区,对于全局对象,常量存放在全局/静态存储区。对于字面值常量(常数),常量存放在常量存储区。

const修饰成员函数的目的是什么?

const修饰的成员函数表明函数调用不会对对象做出任何更改。事实上如果确认不会对对象做更改,就应该为函数加上const限定,这样无论是const对象还是普通对象都可以调用该函数。

如果是两个函数一个带const,一个不带,这相当于函数的重载

隐式类型转换

对于内置类型,低精度的变量给高精度变量赋值会发生隐式类型转换,其次,对于只存在单个参数的构造函数的对象构造来说,函数调用可以直接使用该参数传入,编译器会自动调用其构造函数生成临时对象。

class Complex{
private:
    double real;
    double image;
public:
    Complex(double _real,double _image):real(_real),image(_image){}
    //转换构造函数
    Complex(double _real):real(_real),image(0){}
    //隐式转换函数
    operator double(){ return real; }
}
int main(){
    Complex c1(1.2,3.5);
    complex c2 = 4.4
    double d = 1.8 + c1;
}

只有一个形参的构造函数 Complex(double real) 就是转换构造函数,编译器需要将4.4转换为Complex类型,于是以4.4为形参,调用Complex(double real)构造函数,生成临时对象,然后再调用默认拷贝构造函数为对象c2进行初始化。

C++函数栈空间的最大值

默认是1M,不过可以调整。

“extern C”作用

为了能够正确实现c++代码调用其他c语言代码。加上“extern C”后,会指示编译器这部分代码按C语言的方式进行编译。

由于c++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而c语言不支持函数重载,因此编译c语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

内联函数inline

内联函数是把函数编译好的二进制指令直接复制到函数的调用位置inline仅是对编译器的建议,如果函数太复杂(while),即使声明inline也不是内联函数。

优点:能够提高程序的运行速度,少去了函数调用时间

缺点:会导致可执行文件冗余,牺牲空间来换取时间

分类

  • 显示内联:在函数前加inline
  • 隐式内联:在类中直接定义的成员函数被自动优化成内联函数

内联函数和宏函数的区别

  • 宏函数并不是真正的函数,只是代码的替换,因此不会有参数压栈、出栈以及返回值,也不会进行参数类型检查,因此所有类型都可以使用,存在安全隐患。而内联函数是真正的函数,严格检查参数类型。
  • 内联函数是在编译时展开,直接被嵌入到目标代码,宏函数是在预编译阶段展开,只是一个简单的文本替换过程。
  • 宏在定义时要小心处理宏参数,一般用括号括起来,否则会出现二义性,内联函数不会出现二义性。

new/delete与malloc/free的区别

new/delete是c++的关键字,需要头文件支持,而malloc/free是c语言的库函数,需要编译器支持,后者使用必须指明申请内存空间的大小,并且返回的是void指针需要强制转换,对于类类型的对象,后者不会调用构造函数和析构函数。new的底层分为两步,第一步先调用operator new申请空间,内部实现其实就是malloc,第二步调用对象的构造函数

define和const的区别

  • define是在编译预处理阶段起作用,const是在编译和程序运行阶段起作用
  • define定义的宏常量是没有类型的,只是简单的替换,const修饰的只读常量是有类型的
  • define定义的常量在内存中有若干拷贝,const修饰的常量只有一份拷贝,存放在静态区
  • define可防止重复定义

C++中的RTTI机制

这是一个超链接

1. typeid函数

typeid返回的是一个type_info对象,对于c++的内置数据类型,typeid可以方便的输出它们的数据类型。

#include <iostream>
#include <typeinfo>
using namespace std;
class A
{
public:
     void Print() { cout<<"This is class A."<<endl; }
};
class B : public A
{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
int main()
{
    int a = 1;
    A *pa = new B();
    cout<<typeid(a).name()<<'\n';//int
    cout<<typeid(pa).name()<<'\n';//class A*
    cout<<typeid(*pa).name()<<'\n';//class A
    return 0;
}

pa是一个A类型的指针,所以输出为指针类型,*pa表示pa指向的对象类型,所以输出class A。

但是pa明明指向的B,为什么得到的却是class A?

  • 当类中不存在虚函数时,typeid是编译时期的事情,也就是静态类型
  • 当类中存在虚函数时,typeid是运行时期的事情,也就是动态类型
#include <iostream>
#include <typeinfo>
using namespace std;
class A
{
public:
     virtual void Print() { cout<<"This is class A."<<endl; }
};
class B : public A
{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
int main()
{
     A *pA = new B();
     cout<<typeid(pA).name()<<endl; // class A *
     cout<<typeid(*pA).name()<<endl; // class B
     return 0;
}

2. type_info类里的比较运算符

使用type_info类中重载的==和!=比较两个对象的类型是否相等,通常用于比较两个带有虚函数的类的对象是否相等

#include <iostream>
#include <typeinfo>
using namespace std;
class A{
public:
     virtual void Print() { cout<<"This is class A."<<endl; }
};
class B : public A{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
void Handle(A *a){
     if (typeid(*a) == typeid(A)) {cout<<"I am a A truly."<<endl;}
     else if (typeid(*a) == typeid(B)) {cout<<"I am a B truly."<<endl;}
     else {cout<<"I am alone."<<endl;}
}
int main(){
     A *pA = new B();
     Handle(pA);
     delete pA;
     return 0;
}
/*
I am a B truly.
*/

3. dynamic_cast机制

dynamic_cast主要用于在多态的时候,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类指针(引用)转换为派生类指针(引用)。

当类中存在虚函数时,编译器就会在类的成员变量中添加一个指向虚函数表的vptr指针,每一个class所关联的type_info对象也经由虚函数表被指出来,通常这个type_info对象放在表格的第一个slot。当我们进行dynamic_cast时,编译器会帮我们进行语法检查。如果指针的静态类型和目标类型相同,那么就什么事情都不做;否则,首先对指针进行调整,使得它指向虚函数表,并将其和调整之后的指针、调整的偏移量、静态类型以及目标类型传递给内部函数。其中最后一个参数指明转换的是指针还是引用。两者唯一的区别是,如果转换失败,前者返回NULL,后者抛出bad_cast异常。对于在typeid函数的使用中所示例的程序,使用dynamic_cast进行更改,代码如下:

#include <iostream>
#include <typeinfo>
using namespace std;
class A{
public:
     virtual void Print() { cout<<"This is class A."<<endl; }
};
class B : public A{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
class C : public A{
public:
     void Print() { cout<<"This is class C."<<endl; }
};
void Handle(A *a){
     if (dynamic_cast<B*>(a)) {cout<<"I am a B truly."<<endl;}
     else if (dynamic_cast<C*>(a)) {cout<<"I am a C truly."<<endl;}
     else{ cout<<"I am alone."<<endl;}
}
int main(){
     A *pA = new B();
     Handle(pA);
     delete pA;
     pA = new C();
     Handle(pA);
     return 0;
}
/*
I am a B truly.
I am a C truly.
*/

4. RTTI底层实现的原理

在一个类的虚函数表里面添加了一个新的条目。

C语言如何进行函数调用?

每个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。函数参数的压栈顺序为从左往右。

C++如何处理返回值?

生成一个临时变量,把它的引用作为函数参数传入函数内。

NULL和nullptr的区别

在c语言,NULL是个宏定义为void*(0)的东西

int *p = NULL;//这里发生了隐式转换,void*转化为int*类型

NULL在c++中是数字0,因为c++中void*类型是不允许隐式转化成其他类型

nullptr在c++中表示空指针

2、容器和算法

STL的基本组成

  • 容器:一些封装数据结构的模板类。

  • 算法:STL提供了非常多的数据结构算法,它们都被设计成一个个的模板函数。

  • 迭代器:对容器中数据的读写,是通过迭代器完成的。

  • 函数对象(仿函数):如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象。

    本质:函数对象是一个,不是一个函数

  • 适配器:可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。

  • 内存分配器:为容器类模板提供自定义的内存申请和释放功能,并不常用。

vector

vector底层实现是数组连续存储结构。每次push_back,会比较size和capacity,相等的话会新开辟一片两倍的capacity的空间,然后把元素迁移到新空间,释放掉原来的空间。

函数接口:

  • size:当前已有的对象元素个数
  • capacity:容器预留的容量,最多可放的个数
  • resize:调整元素个数,小于当前size会将尾部元素去掉,大于当前size,添加的元素默认初始化0,超过capacity,capacity也会增加
  • reserve:调整容器预留容量,但不会改变size

在底层。vector使用了三个指针:

  • first:起始位置
  • last:最后一个元素位置
  • end:整个容器所占内存空间的末尾

size = last - first capacity = end - first

emplace_back()和push_back的区别

emplace_back()是在vector的内存中原地构造,而push_back()是在外部构造后,通过移动或者拷贝到vector中。所以emplace_back()效率会更优。

list

list底层实现是双向链表非连续存储结构,每个元素维护一对前后向指针,所以支持正序和逆序遍历。

插入和删除高效,但随机访问效率低。每个元素需要维护额外的两个指针,空间开销较大。

list每次插入都会新开辟一个元素单位的空间存放。

list的优缺点

list的优点:

  • 采用动态存储分配,不会造成内存浪费和溢出
  • 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素

list的缺点:

  • 链表灵活,但是空间(指针域) 和 时间(遍历)额外耗费较大

deque

deque是双端队列,连续存储结构,它提供了两级数组结构,第一级完全类似vector,代表容器,第二级维护了一个容器的首地址。不仅拥有vector的所有功能,还支持高效的首部/尾部的添加和删除。可以理解为合并了vector和list的功能。deque容器的迭代器也是支持随机访问的。

deque内部工作原理

deque内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据

中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间

C++总结

vector、list、deque使用场景

  • 要求随机存取,不在乎首部/尾部插入和删除效率,用vector
  • 要求大量增删,不要求随机存取,用list
  • 要求随机存取和首部/尾部大量增删,用deque

string和char * 区别

  • char * 是一个指针
  • string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器。

deque与vector区别

  • vector对于头部的插入删除效率低,数据量越大,效率越低
  • deque相对而言,对头部的插入删除速度回比vector快
  • vector访问元素时的速度会比deque快,这和两者内部实现有关

map和set的实现和区别

map和set都是C++的关联式容器,其底层实现都是红黑树(RB-Tree)。由于map和set所开放的各种操作接口,RB-Tree也都提供了,所以几乎所有的map和set的操作行为,都只是转调RB-Tree的操作行为。

二者的区别:

  • map中的元素是key-value对,关键字起到索引的作用,值则表示与索引相关联的数据;set每个元素只包含一个关键字
  • set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。因为map和set是根据关键字排序来保证其有序性,如果修改key的话,那么需要删除该key,然后调节平衡,再插入修改后的键值,调节平衡。这样一来,严重破坏map和set的结构,导致迭代器失效,不知道指向改变前还是改变后的位置。
  • map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中。

map和unordered_map

二者都属于关联式容器,底层实现都是红黑树,所有元素都是pair,同时拥有键值(key)和实值(value)。pair的第一元素为键值,第二元素为实值。所有元素会根据元素的键值自动排序。

map

映射。map不允许键值重复。适用于有序键值对不重复映射。

unordered_map

多重映射。multimap允许键值重复。适用于有序键值对可重复映射。

set和unordered_set

set

基于红黑树实现,所有元素会根据元素的键值自动排序。不允许容器中有重复的元素

unordered_set

基于哈希表,数据插入和查询时间复杂度几乎是常数时间,代价就是消耗较多的内存,无自动排序功能。底层实现上,使用一个下标范围比较大的数组来存储元素,形成很多的桶,利用hash函数对key进行映射到不同区域进行保存。允许容器中有重复的元素

STL迭代器删除元素

  • 序列容器vector,deque来说,使用erase(itertor),后面每个元素的迭代器都会失效,但后边每个元素都会往前移动一个位置,erase返回会返回下一个有效的迭代器。
  • 关联容器map,set来说,使用erase(itertor),当前的迭代器失效,因为结构是红黑树,不会影响下一个迭代器,所以在调用erase之前记录下一个元素的迭代器即可。
  • list来说,它使用不连续分配的内存,并且erase方法会返回下一个有效的迭代器。

迭代器的作用

Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素而又无需暴露该对象的内部表示。

由于Iterator模式有以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如stl的list、vector、stack等容器类及ostream_iterator等扩展iterator。

迭代器和指针的区别

迭代器不是指针,而是类模板。它只是通过重载指针的一些操作符(->、*、++、–等)模拟了一些指针的功能。迭代器封装了指针,是一个“可遍历stl容器内全部元素或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,可以根据不同类型的数据结构来实现不同的++、–等操作。

迭代器返回的是对象引用而不是对象的值,所以const只能输出迭代器使用*取值后的值而不能直接输出其自身。

迭代器产生的原因

iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

resize和reserve的区别

resize():改变当前容器内含有元素的数量(size()),size变为len,如果size<len,那么容器新增(len-size)个元素,默认为0,push_back(2)后,2放在下标为len上,size为len+1。

reserve():改变当前容器的最大容量(capacity()),不会生成元素,只是确定这个容器允许放入的对象大小,capacity变为len,如果capacity<len,那么会重新分配一个能存下len个对象的空间,然后把之前size个对象通过copy construtor复制过来,销毁之前的内存。

vector<int>a;
a.reserve(100);
a.resize(50);
cout<<a.size()<<' '<<a.capacity()<<'\n';// 50 100
a.resize(150);
cout<<a.size()<<' '<<a.capacity()<<'\n';//150 150
a.reserve(50);
cout<<a.size()<<' '<<a.capacity()<<'\n';//150 150
a.resize(50);
cout<<a.size()<<' '<<a.capacity()<<'\n';//50 150

STL的分配器allocator

这是一个超链接

STL的分配器用于封装STL容器在内存管理上的底层细节。

在C++中,内存配置和释放如下:

  • new运算分两个阶段:(1)调用::operator new适配内存;(2)调用对象构造函数构造对象内容
  • delete运算分两个阶段:(1)调用对象析构函数;(2)调用::operator delete释放内存

为了精密分工,STL中对象的构造由::construct()负责,对象析构由::destroy()负责;内存配置由alloc::allocate()负责,内存释放由alloc::deallocate()负责;此外,STL还提供一些全局函数,用来对大块内存数据进行操作。

同时为了提升内存管理效率,减少申请小内存造成的内存碎片问题,SGI STL采用两级分配器,当分配空间大小超过128bytes时,使用第一级空间配置器,直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放;当分配空间小于128bytes时,使用第二级空间配置器,采用内存池技术,通过空闲链表来管理内存。

3、类和数据抽象

C++中类成员的访问权限

C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限。类内都能访问,类外只能访问public属性的成员,派生类可以访问基类protected属性的成员。

struct和class的区别

在C++中,可以用struct和class定义类,都可以继承。区别在于

  • struct的默认继承权限和默认访问权限是public,而class的默认继承权限和访问权限是private。struct可以继承class,同样class也可以继承struct,继承的默认权限取决于子类

  • class可以定义模板类参数,而struct不用于定义模板参数

    template <class T,int i>
    
  • 如果没有定义构造函数,struct可以用大括号初始化,class需要所有成员变量都是public才可使用大括号初始化,如果定义了构造函数,二者均不可使用大括号进行初始化。

C++结构体和类大小的计算

结构体

这是一个超链接

没有成员的结构体占1个字节,为了使得每个实例在内存中都有一个独一无二的地址。

偏移量:结构体变量中成员的地址和结构体变量地址的差。

存储变量时地址要求对齐,编译器在编译程序时会遵循两条原则

  • 结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
  • 结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。
struct stru1 {
    int a;  //start address is 0
    char b;  //start address is 4
    int c;  //start address is 8
};
struct stru2  
{  
      int i;  //start address is 0
      short m;  //start address is 4
};
cout<<sizeof(stru2)<<'\n'//8

对于嵌套结构体或者结构体包含数组,需将其展开,原则变为

  • 展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。
  • 结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。
struct stru3 
{  
    char i;  //0
    struct{  
         char c;// 4  
         int j;  //8
    } tt;   
    char a;//9
    char b;//10
    char d;//11
    char e;//12
    //或者char a[4]
    int f;//16  
};
cout<<sizeof(stru3)<<'\n';//20

声明的类只是一种类型定义,类的大小指的是类的对象大小。

关于类/对象大小的计算

  • 类大小的计算遵循结构体的对齐原则
  • 类大小与普通数据成员有关,与成员函数和静态成员无关。
  • 虚函数对类的大小有影响,因为有虚函数表指针
  • 虚继承对类的大小有影响,因为有虚基类指针
  • 空类大小为1

空类

  • 继承空类,空基类的1个字节不会加到派生类

  • 一个类包含一个空类对象数据成员,其数据成员算1字节

    class Empty {};
    class HoldsAnInt {
        int x;
        Empty e;
    };
    cout<<sizeof(HoldsAnInt)<<'\n';//8
    

虚函数对类大小的影响

含有虚函数的对象除了基本的数据类型,就是多了一个指向虚函数表的vptr(指针8个字节)。如果是继承,继承几个含有虚函数的类就多几个vptr。派生类中的虚函数地址会存放在第一个虚函数表中。

C++总结C++总结
class A{  
    char ch;     
    virtual void func0()  {  }   
};   
class B{  
    char ch1;  char ch2;  
    virtual void func()  {  }    
    virtual void func1()  {  }   
};    
class C: public A, public B{     
    int c;     
    virtual void func0()  {  }   
    virtual void func1()  {  }  
};  
cout<<sizeof(A)<<'\n';//16 = 8 + 8
cout<<sizeof(B)<<'\n';//16
cout<<sizeof(C)<<'\n';//32 = 8*2 + 8 + 8

虚继承对类大小的影响

虚表指针在整个继承关系*享的,不共享的是指向虚基类的指针

class A {
    int a;
	virtual void myfunA(){}
};
class B:virtual public A{
    virtual void myfunB(){}
};
class C:virtual public A{
    virtual void myfunC(){}
};
class D:public B,public C{
    virtual void myfunD(){}
};
cout<<sizeof(A)<<'\n';//16 = 8 +8
cout<<sizeof(B)<<'\n';//24 = 8 + 8 + 8 指向虚函数表指针+指向虚基类指针+int
cout<<sizeof(C)<<'\n';//24 = 8 + 8 + 8
cout<<sizeof(D)<<'\n';//32 = 8 + 2*8 + 8 指向虚函数表指针+B中指向虚基类指针+C中指向虚基类指针+int

C++类内定义引用数据成员的规则

  • 不能用默认构造函数初始化,必须提供构造函数来初始化引用成员变量
  • 构造函数的形参必须是引用类型
  • 不能在构造函数里初始化,必须在初始化列表中进行初始化

构造函数分为初始化和计算两个阶段,前者对应成员初始化链表,后者对应构造函数体。

class A{
public:
    A(int &target):a(target){
        cout<<"构造函数"<<'\n';
    }
private:
    int a;
}

构造函数列表初始化顺序

类成员的初始化顺序不是按照初始化列表的顺序来的,而是按照类成员的声明顺序

class A{
private:
    int i;
    int j;
public:
    A(int _j,int _i):j(_j),i(_i){}//先初始化i,再初始化j,因为i的声明在j前面
}

构造函数体和初始化列表的区别

构造函数先进行列表初始化,然后才会执行构造函数体。只有列表初始化才叫初始化,函数体内的赋值只能叫初赋值。

而基本的数据类型没有初始化都是默认值,以下三种必须通过初始化列表

  • const修饰的变量。因为必须定义时初始化,不允许先定义后赋值。
  • 引用修饰的变量。因为必须定义时初始化,不允许先定义后赋值。
  • 自定义数据类型

深拷贝和浅拷贝

浅拷贝:两个对象同时指向同一个内存,导致析构时,进行两次析构,同一块内存释放两次,会导致程序崩溃。

深拷贝:调用手写的拷贝构造函数,为新对象开辟一个新内存

类中默认的拷贝构造函数都是浅拷贝。

为什么重载不可以根据返回值类型

比如两个函数只有返回值不同,主函数不需要接受这个返回值时,会产生二义性,编译器不知道要调用哪个重载的函数。

4、面向对象与泛型编程

左值引用和右值引用

右值引用是C++11中引入的新特性,它实现了转移语义和精确传递。

有名称的、可以获取到存储地址的表达式即为左值;反之是右值。

int a = 5;//5是右值
int b = a;//a和b都是左值
5 = a;//错误,5不能作为左值
const int &c = 10;//常量左值引用操作右值,不可修改
int &&d = 10;//右值引用,必须初始化且只能使用右值进行初始化
d = 20;//右值引用可以修改
cout<<d<<'\n';//20

作用

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
  • 能够更简洁明确地定义泛型函数

左值引用和右值引用的区别

  • 左值可以寻址,右值不可以
  • 左值可以被赋值,右值不可以,只能用来给左值赋值
引用类型 可以引用的值类型 使用场景
非常量左值引用 常量左值引用 非常量右值引用 常量右值引用
非常量左值引用 T F F F
常量左值引用 T T T T 常用于类中构建拷贝构造函数
非常量右值引用 F F T F 移动语义、完美转发
常量右值引用 F F T T 无实际用途

为什么拷贝构造函数的参数要用左值引用,不用值传递

  • 如果用值传递会陷入无限的死循环。因为如果是值传递,那么从实参再运行拷贝构造函数给形参,这里又拷贝了。
  • 不考虑死循环,在效率上用引用也高于值传递,用值传递还需要拷贝一份。

移动语义和完美转发

这是一个超链接

c++11引入了移动语义和完美转发,两者都基于右值引用

移动语义

转移语义:将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制,对象的移动语义需要实现移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。

无条件地将参数转为右值

作用:减少资源开销和提高效率。

比如主函数接收一个函数返回一个类的实例对象,首先需要调用普通构造函数生成实例对象,因为对象是这个函数内部的,所以需要调用拷贝构造函数生成临时对象返回给主函数,主函数又会调用拷贝构造函数获得临时对象的值。进行了两次拷贝,消耗时间。而移动语义通过移动构造函数,将资源所有权给了临时对象,临时对象又转移给主函数部分。本质就是像交换(swap)。

template <typename T>
class A{
private:
    int m_size;
    T* m_array;
public:
    //移动构造函数
    A(A&& rhs):m_size{rhs.m_size},m_array{rhs.m_array}{
        rhs.m_size = 0;
        rhs.m_array = nullptr;
    }
}

也可以使用std::move去实现

void swap(T& a, T& b){
    T tmp{std::move(a)};   // 调用移动构造函数
    a = std::move(b);       // 调用移动赋值运算符
    b = std::move(tmp);     // 调用移动赋值运算符
}

完美转发

不改变参数的左右值类型

比如一个string的右值传递给wrapper,param为右值引用,一旦传入,param就变成左值,丢失了右值属性,使用std::forward确保传入foo的值还是一个右值。

// 目标函数
void foo(const string& str);   // 接收左值
void foo(string&& str);        // 接收右值
template <typename T>
void wrapper(T&& param){
    foo(std::forward<T>(param));  // 完美转发
}

5、编译与底层

C++编译过程

这是一个超链接

主要分为4个过程

  • 编译预处理:对源代码文件中文件包含关系(头文件),预编译语句(宏定义)进行分析和替换,生成预编译文件。(.c文件->i文件)
  • 编译阶段:通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将经过预处理后的预编译文件转换化特定汇编代码,生成汇编文件。(.i文件->.s文件)
  • 汇编阶段:将编译阶段生成的汇编文件转化成目标机器指令,生成可重定位目标文件。(.s文件->.o文件)
  • 链接阶段:将多个目标文件及所需要的库链接成最终的可执行目标文件。(.o文件->可执行文件)

动态链接和静态链接

静态链接:在程序运行之前,将库函数全部载入到可执行的目标文件之中,这样子程序运行就脱离了库函数,优点是可移植性强,缺点也很明显,将不需要的库函数导入进去,消耗了太多内存。

动态链接:在程序运行之前,到需要调用的库函数的时候,去看其他执行的文件有没有,如果存在的话,共享这个库函数,直接调用,等于库函数只有一份内存拷贝,没有的话再进行动态链接,这样会耗费一些时间,并且可移植性差,但是与静态链接相比占用内存少,并且如果修改了库函数的话,静态链接需要重新编译,而动态链接则不需要,因为调用的是库函数的接口。

头文件用双引号和尖括号的区别

编译器预处理阶段查找头文件的路径不一样

  • 双引号:当前头文件目录->编译器设置的头文件路径->系统变量指定的头文件路径
  • 尖括号:编译器设置的头文件路径->系统变量指定的头文件路径

如何采用单线程的方式处理高并发

在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件。

C++内存管理

内存分区

在C++中,一般内存主要分为五部分:

  • 代码区:存放程序代码,即CPU执行的机器代码,并且是只读的。

    代码区的地址:函数地址、程序入口地址、程序的名字

    函数名称是一个指针,可以通过查询函数名称所处的内存地址,查询函数存放的地址

    void test(){
        printf("main:0x%p\n",main);//打印main函数的存放地址
    }
    
  • 常量区:存放常量,不允许修改。

  • 静态区(全局区):静态变量和全局变量的存储区域是一起的,一旦静态区的内存被分配,,静态区的内存直到程序全部结束之后才会被释放。

  • 堆区:由程序员调用malloc()函数来主动申请的,需使用free()函数来释放内存,若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏。

  • 栈区:存放函数内的局部变量,形参和函数返回值,由编译器自动分配和释放。

    栈区之中的数据的作用范围过了之后,系统就会回收自动管理栈区的内存(分配内存 , 回收内存),。

C++总结

堆和*存储区的区别

  • *存储是C++中通过new与delete动态分配和释放对象的抽象概念,而堆(heap)是C语言和操作系统的术语,是操作系统维护的一块动态分配内存。
  • new所申请的内存区域在C++中称为*存储区。由堆实现的*存储,所以可以说new所申请的内存区域在堆上。
  • 堆与*存储区还是有区别的,它们并非等价。

栈和堆的区别

  • 栈是系统自动分配的,堆是程序员自己申请的
  • 栈在内存中是一块连续的地址,向低地址扩展,内存空间较小,堆在内存空间是不连续的,向高地址扩展,内存空间较大
  • 栈的申请效率高,堆的效率较低,使用起来方便但容易产生内存碎片
  • 栈中主要存放局部变量、函数参数,堆的内容由程序员自己安排

内存泄漏与内存溢出

内存泄漏:一般指的就是堆内存的泄露,因为堆的内容是程序员自己安排的。内存泄漏即new出来的东西没有delete,造成了内存被占用。

内存溢出:指所剩余的可申请内存空间小于要申请的空间,比如栈满时候进栈,就产生了内存溢出。

内存泄漏的分类

  • 堆内存泄漏。使用new或者malloc没有使用delete或者free释放。
  • 系统资源泄漏。程序使用系统分配的资源没有使用相应的函数释放掉。
  • 没有将基类的析构函数定义为虚函数。子类的析构函数不会被调用,子类资源无法释放。

如何解决内存泄漏

使用智能指针,自动删除分配的内存。

内存对齐的作用

  • 从硬件上来说,cpu读取内存不是一个字节一个字节的读,而是一块一块的读取,所以内存对齐有利于减少读取时间,本质上就是空间换时间。
  • 从平台上来说,在有些平台上只能在特定位置读取一定的字节块,内存对齐就统一这样的规范,而不会使得在不同平台上的读取结果不同。

可以通过 # pragma pack(1) 取消内存对齐

内存优化

这是一个超链接

段错误

段错误通常发生在访问非法内存地址的时候,存下以下几种情况:

  • 使用野指针
  • 试图修改字符串常量的内容

6、C++11

C++11有哪些新特性

  • auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
  • nullptr关键字:一种特殊类型的字面值,它可以被转换成任意其他的指针类型,而null一般被宏定义为0
  • 智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理问题
  • 初始化列表:使用初始化列表来对类进行初始化
  • 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节约资源
  • atomic原子操作用于多线程资源互斥操作
  • 新增STL的array和tuple容器
上一篇:C语言#denfine 与#typedef的区别


下一篇:[源码解析] PyTorch 分布式 Autograd (2) ---- RPC基础