【C++】基础知识


一、C和C++的区别

  • C是面向过程的编程,特点是函数;C++是面向对象的编程,特点是类
  • C主要应用在嵌入式开发,驱动开发和硬件直接打交道的领域;C++可以应用于应用层的开发,用户界面和操作系统直接打交道的领域
  • C++继承了C的底层操作特性,增加了面向对象的机制,增加了泛型编程,异常处理,运算符重载,命名空间等特性

二、C++程序的编译过程

2.1 编译分为四个过程:

  • 编译预处理:处理以#开头的指令(宏指令,条件编译指令,头文件等)
  • 编译,优化:将源码 .cpp 文件翻译成 .s 汇编代码
  • 汇编:将汇编代码 .s 翻译成机器指令 .o 文件
  • 链接:汇编程序生成的目标文件并不会立即执行,可能有源文件中的函数引用了另一个源文件种定义的符号或者调用了某个库中的函数。链接就是将这些目标文件连接成一个整体,从而生成可执行的程序 .exe 文件。
    参考链接
    编译过程

2.2 链接

  • 静态链接:链接器在链接静态链接库的时候是以目标文件为单位的。每个源文件都是独立编译的,需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。

    • 缺点: 浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难)
    • 优点:执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容
  • 动态链接:动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件

    • 缺点: 但是动态链接是在程序运行时,每次执行都需要进行链接,性能会有一定的损失
    • 优点:节省内存、更新方便
      参考链接

三、内存分配方式以及区别

3.1 四区

1. 静态(全局)储存区:静态变量和全局变量的储存区域是一起的,一旦静态区的内存被分配,那么其内存直到程序全部结束之后才会被释放
2. 代码区: 存放程序代码的, 即CPU执行的机器指令,并且是只读的
3. 栈区:存放函数内的局部变量,形参和函数返回值。栈区之中的数据的作用范围过了之后,系统就会自动回收管理栈区的内存(自动分配内存 , 回收内存)
4. 堆区:由程序员调用函数来主动申请和释放内存,若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏

3.2 堆栈的主要区别

差别 栈区 堆区
申请方式 由操作系统自动分配释放 由程序员分配释放
申请效率 栈由系统自动分配,速度较快 堆是由new分配的内存,需要查找足够大的内存大小,一般速度比较慢
碎片问题 是一块连续的内存的区域,不会产生内存碎片 是不连续的内存区域,频繁的new/malloc会造成大量的内存碎片
空间大小 和栈一样,是一块连续的内存空间,空间小 堆是不连续的内存空间,数据结构是链表,空间大
存放内容 存放函数的参数值(从右往左入栈),局部变量(非静态)、函数返回地址等值 比较灵活,由程序员安排
分配方式 有静态分配也有动态分配,静态分配是由编译器完成,动态分配由alloca函数分配,编译器自动释放,无需程序员实现。 动态分配,没有静态分配。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序

3.3 程序执行过程


Code Segment(代码区):存放可执行程序的机器码
Data Segment (数据区):存放已初始化的全局和静态变量, 常量数据(如字符串常量)
BSS(Block started by symbol):存放未初始化的全局和静态变量
Heap(堆):从低地址向高地址增长。容量大于栈,程序中动态分配的内存在此区域
Stack(栈):从高地址向低地址增长。由编译器自动管理分配。程序中的局部变量、函数参数值、返回变量等存在此区域

3.3.1 函数栈

函数调用步骤为:

  • 参数入栈:将函数的参数从右向左依次压入系统栈中
  • 返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行
  • 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处
  • 栈帧调整:
    • 保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)
    • 将当前栈帧切换到新栈帧。(将ESP值装入EBP,更新栈帧底部)
    • 给新栈帧分配空间。(把ESP减去所需空间的大小,抬高栈顶)

假设代码运行的顺序为mian函数调用fun_A,然后fun_A调用fun_B,为了方便理解栈帧变化状态,在下图中的栈底位于最下方。

  1. 在main函数调用func_A的时候,首先在自己的栈帧中压入函数返回地址,然后为func_A创建新栈帧并压入系统栈在func_A
  2. 调用func_B的时候,同样先在自己的栈帧中压入函数返回地址,然后为func_B创建新栈帧并压入系统栈
  3. 在func_B返回时,func_B的栈帧被弹出系统栈,func_A栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到func_A代码区中执行
  4. 在func_A返回时,func_A的栈帧被弹出系统栈,main函数栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址跳到main函数代码区中执行

四、C++中指针和引用的区别

4.1 定义和性质区别

  • 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已
    int a=1;int *p=&a; //指针变量p指向整型变量a的储存单元
    int a=1;int &b=a; //整形变量a的引用b,b和a是一个东西,在内存中占有同一个储存单元
    sizeof(指针)得到的是指针本身的大小;sizeof(引用)得到的是所指向的变量(对象)的大小
  • 可以有const指针,但是没有const引用(引用本来就不可变,没有意义)
  • 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变
  • 指针的值可以为空,但是引用的值不能为null,并且引用在定义的时候必须初始化
  • 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了
  • 指针可以有多级,但是引用只能是一级(int **p合法 而 int &&a是不合法的)

4.2 作为函数参数进行传递时的区别

4.2.1 指针作为函数参数

  1. 用指针传递参数,可以实现对实参进行改变的目的,是因为传递过来的是实参的地址,因此使用*a实际上是取实参的内存单元里的数据,即是对实参进行改变

    void swap(int *a,int *b)
    {					//传进实参的地址
    	int temp=*a;	//取出a指针指向内存中的数据
    	*a=*b;			
    	*b=temp;
    }
    int main(void)
    {
    	int a=1,b=2;
    	swap(&a,&b);	//内存单元a中的数据改变了
    	cout<<a<<" "<<b<<endl;
    	//输出2 1
    }
  2. 将指针作为参数进行传递时,事实上也是值传递,只不过传递的是地址。当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参

    void test(int *p)
    {
      int a=1;
      p=&a;			//此时的p为实参的一个拷贝形参
      				//形参的值发生改变,而实参中的值不会变化。
      cout<<p<<" "<<*p<<endl;
    }
    
    int main(void)
    {
        int *p=NULL;
        test(p);		//main函数中的p和test函数中使用的p不是同一个变量
        if(p==NULL)
        cout<<"指针p为NULL"<<endl;
        //输出 0x22ff44 1
        //    指针p为NULL
    }

4.2.2 将引用作为函数的参数进行传递

  • 引用作为函数参数进行传递时,实质上传递的是实参本身,而不是实参的一个拷贝,因此对形参的修改其实是对实参的修改
    void test(int *&p)
    {
      int a=1;
      p=&a;
      cout<<p<<" "<<*p<<endl;
    }
    
    int main(void)
    {
        int *p=NULL;
        test(p);			//此时p与test中的p是同一个东西
        if(p!=NULL)
        cout<<"指针p不为NULL"<<endl;
    }

五、new 和 delete

new 和 delete 不是函数,它们都是 C++ 定义的关键字,通过特定的语法可以组成表达式

5.1 malloc/free和new/delete的区别和联系

  1. malloc/free只是动态分配内存空间/释放空间。而new/delete除了分配空间还会调用构造函数和析构函数进行初始化与析构
  2. malloc/free是C/C++标准库的函数,new/delete是C++操作符
  3. malloc/free需要手动计算类型大小且返回值为void*,new/delete可自动计算类型的大小,返回对应类型的指针。
  4. alloc/free管理内存失败会返回0,new/delete等的方式管理内存失败会抛出异常。

5.2 new/delete的背后机制



new表达式并不直接开辟内存出来,而是通过调用operator new来获得的内存,而operator new获得的内存实质上还是用malloc开辟出来的。同理,delete表达式也不是直接去释放掉内存,而是通过调用operator delete来调用free。

5.3 new []/delete []的背后机制


new []表达式原理和new表达式一样,如果数组中元素是自定义类型,则会多次调用其默认构造函数。同理,delete [],依次调用pA指向对象数组中每个对象的析构函数,调用operator delete[](),它将再调用operator delete。底层用free执行operator delete表达式,依次释放内存

注意:针对简单类型,new[]计算好大小后调用operator new。针对复杂类型,new[]会额外存储数组大小,里面存放着对象个数。所以delete[] 删除时,将new[] 返回的地址再往前移4个字节便可以拿到要析构的对象个数了。

5.4 new/delete, new []/delete[], malloc/free一定要配套使用


如果不配套使用,则会发生:

//malloc/delete的组合
void Test1()
{
    AA* p1 = (AA*)malloc(sizeof(AA));   
    delete p1;               //没有报错,但不建议采用,容易引起混淆
    AA* p2 = (AA*)malloc(sizeof(AA));   
    delete[] p2;			//报错,同上,释放位置也不对
}
//delete, delete[] 之间误用(值得注意)
void Test2()
{
    AA* p3 = new AA;    
    free(p3);	 	//不报错,但未清理干净。p3的构造函数开辟的空间没有被释放
    
    AA* p4 = new AA[10];   
    delete p4; 	//存在问题,释放位置被后移了4字节。同时只调用了一次析构函数    
    AA* p5 = new AA;        
    delete[] p5; 	//报错 非法访问内存
} 


六、内存泄漏、内存溢出、野指针

6.1 内存泄漏

内存泄漏是指我们在内存中申请(new/malloc)了一块内存,但是没有去动手释放(delete/free)内存,导致指针已经消失,而指针所指向的空间还被占用,系统就已经不能控制这块空间了。
发生原因:

  1. 程序循环new创建出的对象没有及时delete掉,导致内存泄漏。
  2. delete掉一个void* 类型的指针,导致没有调用到对象的析构函数,析构的所有清理工作都没有去执行从而导致内存的泄漏。
  3. new创建一组对象数组,内存回收的时候缺只调用了delete而非delete[]来处理,导致只有对象数组的第一个对象的析构函数得到执行并回收了内存,而其他对象所占内存得不到回收。
    class Object {
    private:
        void* data;
        const int size;
        const char id;
    public:
        Object(int sz, char c):size(sz), id(c){
    	    data = new char[size];
    	    cout << "Object() " << id << " size = " << size << endl;
        }
        ~Object(){
    	    cout << "~Object() " << id << endl;
    	    delete []data;
        }
    };
    int main() {
    	Object* a = new Object(10, 'A');//Object*指针指向一个Object对象;
    	void* b = new Object(20, 'B');//void*指针指向一个Object对象;
    	
    	delete a;//执行delete,编译器自动调用析构函数;
    	delete b;//执行delete,编译器不会调用析构函数,导致data占用内存没有得到回收;
    	
    	Object1* arry1 = new Object1[100];//创建包含100个Object1的对象数组arry1并返回数组首地址;
    	Object1* arry2 = new Object1[100];//创建包含100个Object1的对象数组arry2并返回数组首地址;
    	delete []arry1;//回收了数组arry1里的所有对象动态创建时占用的内存空间;
    	delete  arry2;//回收了数组arry2里的第一个对象动态创建时占用的内存空间,导致其他99个对象的内存空间泄露;
    }

6.2 内存溢出

内存溢出out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请一个int,但是给其存了long才能存得下的数,那就是内存溢出。
发生原因:

  1. 内存中加载的数据量过于庞大;
  2. 代码中存在死循环;
  3. 递归调用太深,导致栈溢出;
  4. 内存泄漏最终导致内存溢出。

6.3 野指针

指向已经删除的对象或者申请访问受限内存区域的指针,称为野指针。
发生原因:

  1. 指针变量未初始化:指针变量在被创建未初始化时,并不是空指针,它的缺省值是随机的,会乱指一气。所以指针变量在创建同时就应对其进行初始化,要么将指针设置为NULL,要么让其指向一个合法的内存。
  2. 指针释放之后未置空:有时指针在free或者delete之后未赋值NULL,有可能被误以为是合法的指针,不能进关注free和delete后的指针名,他们只是将指针所指向的内存空间释放掉而已,但并没有把指针自身消灭,此时,指针指向的就是“垃圾”内存。被释放掉内存空间的指针应该立即将其置为NULL,防止产生野指针。
  3. 指针操作超越变量作用域
    class A {
    public:
    	void Func(void){ cout << “Func of class A<< endl; }
    };
    class B {
    public:
    	A *p;
    	void Test(void) {
    		A a;
    		p = &a; // a 的生命期 ,只在这个函数Test中,而不是整个class B
    	}
    	void Test1() {
    		p->Func(); // p 是“野指针”
    	}
    };
    

七、C++中的类

7.1 C++中struct和class的区别

总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别:

  • 最本质的一个区别就是默认的访问控制默认的继承访问权限。struct 是 public 的,class 是 private 的。
  • struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。

7.2 类中对象分布sizeof

一个Class对象占用的内存空间
= 非静态成员变量总合 + 编译器作出的数据对齐处理 + 为了支持虚函数产生的额外负担。

  1. 一个空类sizeof (1)
  2. 一个类中有static对象的sizeof(1)
  3. 一个空类继承了一个基类的sizeof(基类大小)
  4. 多重继承时的内存分配(多重继承以及虚继承时的分配)
    • 非虚继承的多重继承就是将多个父类的大小叠加并加上自己的内存大小(继承父类的虚函数表指针)
    • 虚继承会在最子类中生成一个虚函数表指针,也保留了父类的虚函数表指针。

7.3 空类自带六个函数

对于空类,编译器不会生成任何的成员函数,只会生成1个字节的占位符。
编译器只会在需要的时候生成6个成员函数:一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针。

class Empty
{
public:
Empty(); 		// 默认构造函数
Empty( const Empty& ); // 默认拷贝构造函数
~Empty(); 		// 默认析构函数
Empty& operator=( const Empty& ); // 默认赋值运算符
Empty* operator&(); 			  // 默认取址运算符
const Empty* operator&() const;   // 默认取址运算符 const

Empty(Empty&& );				  //默认移动构造函数
Empty& operator= (const Empty&&); //默认重载移动赋值操作符
};

C++11中多了两个默认函数:默认移动构造函数和默认重载移动赋值操作符

7.4 必须使用初始化列表初始化数据成员的场合

  1. 需要初始化const修饰的类成员或初始化引用成员数据;

  2. 需要初始化的数据成员是对象的情况,并且这个对象只有含参数的构造函数,没有无参数的构造函数;(没有无参构造函数,无法初始化)

  3. 子类初始化父类的私有成员;

7.5 转换函数

可以使用构造函数将一个指定类型的数据转换为类的对象,也可以使用类型转换函数 (type conversion function)将一个类对象转换为其他类型的数据。转换函数基本形式:
operator <T>() const { //实现转换的语句 } T为类型名称
转换函数必须是类的成员函数,转换函数不能声明返回类型,形参列表必须为空,类型转换函数通常应该是const

class Complex
{
public:
   Complex( ){real=0;imag=0;}
   Complex(double r,double i){real=r;imag=i;}
   operator double( ) {return real;} //类型转换函数
private:
   double real;
   double imag;
};

int main( )
{
   Complex c1(3,4),c2(5,-10),c3;
   double d;
   d=2.5+c1;	//要求将一个double数据与Complex类数据相加,此时程序隐式调用类型转换函数将complex类转化为double
   cout<<d<<endl;
   return 0;
}

7.6 深拷贝浅拷贝

浅拷贝就是两个对象的数据成员之间的简单赋值。如果一个类中没有申请内存的资源,比如堆,那么深拷贝和浅拷贝没有区别,例如:

class A
{
public:
	A() {}
    A(int _data) : data(_data){}
private:
    int data;
};
int main()
{
    A a(5);
    A b(a);		//调用默认拷贝构造函数, b.data = a.data;
    return 0;
}

但当对象中有动态分配的资源时,例如:

class A
{
public:
	A(int _size, int _data):size(_size) 
	{
		data = new int(_data);	//有一段动态分配的内存
	}
    ~A()
    {
    	delete data;			//析构释放动态开辟的资源
    	data = nullptr;
    }
private:
    int* data;
    int size;
};
int main()
{
    A a(5);
    A b(a);				//调用默认拷贝构造函数,b.size = a.size
    					//b.data = a.data;这里b的指针data 和a的指针指向了堆上						   //的同一块内存
    return 0;			
  	//a和b析构时,b先把其data指向的动态分配的内存释放了一次,而后a析构时又将这块已经释放过的内存再释放一次。对同一块内存释放执行2次及2次以上的释放会造成内存泄露或者是程序crash!
}

利用深拷贝来解决:
深拷贝就是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象的另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值。

class A
{
public:
	A() {}
    A(int _size,int _data):size(_size) 
	{
		data = new int(_data);	//有一段动态分配的内存
	}
	//拷贝构造的深拷贝
    A(const A& _A)
    {
    	this->size = _A.size;
    	this->data = new int(*_A.data);		//深拷贝
    	
    }
    //拷贝赋值函数的深拷贝
    A& operator= (const A& _A)
    {
    	if(this == &_A)
    		return *this;
    		
    	delete data;
    	this->size = _A.size;
    	this->data = new int(*_A.data);
    	
    	return *this
    }
     ~A()
    {
    	delete data;			//析构释放动态开辟的资源
    	data = nullptr;
    }
private:
    int* data;
    int size;
};
int main()
{
    A a(5);
    A b(a);		//调用深拷贝
    return 0;
}

7.6 类相关问题

1. 父类指针可以指向子类,子类指针不能指向父类
当基类指针指向派生类的时候,只能操作派生类从基类中继承过来的数据。
当派生类指向基类的指针,因为内存空间比基类长,访问的话会导致内存溢出,所以不允许派生类的指针指向基类。

class Base
{
public:
    int a
};
class Child :public Base
{ 
public:
    int b;
}
/*
Base类占内存大小范围:int a; 占4个字节.
Child类占内存大小范围:int a; 加上 int b;一共占8个字节.

基类指针(Base)指向派生类(Child):
Base *p = new Child();  
因为p是Base类型指针,所以*p只能指向Base类的int a(4字节长度),如果存在虚函数,就可以访问到子类函数和变量.
 
Child *c = new Base(); 
当Child指向父类时,如果c对象访问c->b,这样访问b的时候,就越界了.(因为Base父类空间是4个字节大小,不可能访问到8字节处的b变量).*/

通常来说,子类总是含有一些父类没有的成员变量,或者方法函数。而子类肯定含有父类所有的成员变量和方法函数。所以用父类指针指向子类时,没有问题,因为父类有的,子类都有,不会出现非法访问问题。
但是如果用子类指针指向父类的话,一旦访问子类特有的方法函数或者成员变量,就会出现非法,因为被子类指针指向的由父类创建的对象,根本没有要访问的那些内容,那些是子类特有的,只有用子类初始化对象时才会有。

2. 子类可以转型为父类,而父类不能转型为子类

因为子类是从父类继承而来,子类中包含父类中所有成员。在转换成父类的过程中,意味着对子类进行了一个切割,只是将子类中的父类部分赋值给了父类对象。

而如果父类可以转换成子类,意味着将子类中将有一部分是未知的成员。这是不被允许的。

class Animal{
public:
    int animal = 0;
    void breath()
    {
        cout << "animal breath" << endl;
    }
    virtual void eat()
    {
        cout << "animal eat" << endl;
    }
};

class Fish : public Animal
{
public:
    int fish = 1;
    void breath()
    {
        cout << "fish breath" << endl;
    }
    void eat()
    {
        cout << "fish eat" << endl;
    }
};

int main()
{
    Animal *animal1 = new Animal();
    cout << "父类指针指向父类对象" << endl;
    animal1->eat();

    Animal *animal2 = new Fish();
    cout << "父类指针指向子类对象" << endl;
    animal2->eat();
    cout << animal2->animal << endl;

    Fish *fish1 = new Fish();
    cout << "子类指针指向子类对象" << endl;
    fish1->eat();

    //Fish *fish2 = new Animal();

    Animal *animal3;
    animal3 = static_cast<Animal*>(fish1);
    cout << "子类指针转型为父类指针(static)" << endl;
    animal3->eat();

    Animal *animal4;
    animal4 = dynamic_cast<Animal*>(fish1);
    cout << "子类指针转型为父类指针(dynamic)" << endl;
    animal4->eat();
    animal3->animal;

    Fish *fish3;
    fish3 = static_cast<Fish *>(animal1);
    cout << "父类指针转型为子类类指针(static)" << endl;
    fish3->eat();

    Fish *fish4;
    fish4 = dynamic_cast<Fish *>(animal1);
    if(fish4 == NULL)
    {
        cout << "转型失败" << endl;
    }

    system("pause");
    return 0;
}

输出:

父类指针指向父类对象: animal eat
父类指针指向子类对象: fish eat
子类指针指向子类对象: fish eat
子类指针转型为父类指针(static): fish eat
子类指针转型为父类指针(dynamic): fish eat
父类指针转型为子类类指针(static): animal eat
转型失败

文章作者: YukinoKyoU
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 YukinoKyoU !
评论
  目录