C++复习1:一些概念

本文详细讲解了C++内存模型,浅拷贝与深拷贝的区别,异常处理机制,构造函数、拷贝构造函数、析构函数的使用,虚函数和多态性,以及动态绑定的过程。通过实例演示了类的初始化、赋值和构造优化,涵盖了内存对齐、类型安全和友元函数等内容。
摘要由CSDN通过智能技术生成

理论知识

有限状态机

有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。 深入浅出理解有限状态机

内存模型

C分为四个区:堆,栈,静态全局变量区,常量区. C++内存分为5个区域 (堆栈全常代 ):C/C++ 内存模型

浅拷贝和深拷贝

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。 深拷贝开辟出一块新的空间用来存放拷贝的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

#include <iostream>
#include <string.h>
using namespace std;

class Student
{
private:
    int num;
    char *name;
public:
    Student(){
        name = new char(20);
        cout << "Student constructor" << endl;
    };
    ~Student(){
        cout << "Student destructor " << &name << ", "  << (int*)name << endl;
        delete name;
        name = NULL;
    };
    Student(const Student &s){//拷贝构造函数
        //浅拷贝,当对象的name和传入对象的name指向相同的地址
        // name = s.name;
        //深拷贝
        name = new char(20);
        memcpy(name, s.name, strlen(s.name));
        cout << "copy Student" << endl;
    };
};

int main()
{
    {// 花括号让s1和s2变成局部对象,方便测试
        Student s1;
        Student s2(s1);// 复制对象
    }
    system("pause");
    return 0;
}

浅拷贝执行结果:(注意name指向了同一个地址,析构函数释放了两次)

Student constructor
copy Student
Student destructor 0x61fdf8, 0xf71750
Student destructor 0x61fe08, 0xf71750
Process returned -1073740940 (0xC0000374)

深拷贝执行结果:

Student constructor
copy Student
Student destructor 0x61fdf8, 0x1d1770
Student destructor 0x61fe08, 0x1d1750
请按任意键继续. . .

异常

C++中的异常处理机制主要使用trythrowcatch三个关键字 。程序先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,否则异常通过throw可以抛出,并被catch进行捕获。

#include <iostream>
using namespace std;
int main()
{
    double m = 1, n = 0;
    try {
        cout << "before dividing." << endl;
        if (n == 0)
            throw - 1;  //抛出int型异常
        else if (m == 0)
            throw - 1.0;  //拋出 double 型异常
        else
            cout << m / n << endl;
        cout << "after dividing." << endl;
    }
    catch (double d) {
        cout << "catch (double)" << d << endl;
    }
    catch (...) {
        cout << "catch (...)" << endl;
    }
    cout << "finished" << endl;
    return 0;
}
//运行结果
//before dividing.
//catch (...)
//finished

throw可以抛出各种数据类型的信息,抛出异常的语句格式为:throw 表达式。 上面的代码中使用的是数字,但是也可以自定义异常class。catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(...)的方式捕获任何异常(不推荐)。当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。

  • 异常声明列表

在函数声明和定义时,指出所能抛出异常的列表 (也就是,指出可以抛出的异常类型)。

int fun() throw(int,double,A,B,C){...};

这种写法表名函数可能会抛出int,double型或者A、B、C三种类型的异常,如果throw中为空,表明不会抛出任何异常,如果没有throw则可能抛出任何异常 。

  • exception

一些类是从 exception 类派生而来的,如:

bad_typeid:使用typeid运算符,如果其操作数是一个多态类的指针,而该指针的值为 NULL,则会拋出此异常。

bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常

bad_alloc:在用 new 运算符进行动态内存分配时,如果没有足够的内存,则会引发此异常

out_of_range:用 vector 或 string的at 成员函数根据下标访问元素时,如果下标越界,则会拋出此异常

编译过程

1. 预编译

对c源程序中的伪指令(以#开头的指令)和特殊符号进行处理。 预编译程序所完成的基本上是对源程序的“替代”工作,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。.i是预处理后的c文件,.ii是预处理后的C++文件。 处理规则见下:

  1. 删除所有的#define,展开所有的宏定义。
  2. 处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
  3. 处理#include<>预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他 文件。
  4. 删除所有的注释,“//”和“/**/”。
  5. 保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重 复引用。
  6. 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。

2. 编译

把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件(.s文件 ).

  1. 词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
  2. 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
  3. 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分析的语义,相对应的动态语义是在运行期才能确定的语义。
  4. 优化:源代码级别的一个优化过程。
  5. 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
  6. 目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移 来替代乘法运算、删除多余的指令等。

3. 汇编

将汇编代码转变成目标机器指令。 汇编器根据汇编指令和机器指令的对照表一一翻译过来,汇编过程由汇编器as完成。

经汇编之后,产生目标文件.o文件(Windows 下)、xxx.obj(Linux下)。

4. 链接

此阶段将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接。目前已经有一种文件: 1)可重定位文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件; 2)共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接。

静态链接以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件。在使用静态库的情况下,在链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。

主要完成两件事: 1. 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析将每个符号引用与其定义关联起来。 2. 重定位:把每个符号定义与一个内存位置关联起来。

空间浪费(同一个目标文件都在内存存在多个副本);

更新困难(库函数的代码修改了,就需要重新进行编译链接);

运行速度快(因为在可执行程序中已经具备了所有执行程序所需要的任何东西)。

动态链接克服了静态链接的两个缺点,基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样一开始就把所有程序模块都链接成一个单独的可执行文件然后运行。 这需要附带一个动态链接库。

共享库在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为DLL。 它具有特点: 在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中; 在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。

共享库(即使需要每个程序都依赖同一个库,在执行时也只共享同一份副本);

更新方便(更新时只需要替换原来的目标文件,当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来);

运行慢(因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失)。

与类相关

构造函数

默认构造函数和初始化构造函数在定义类的对象,完成对象的初始化工作;复制构造函数用于复制本类的对象;转换构造函数用于将其他类型的变量,隐式转换为本类对象。

#include <iostream>
using namespace std;

class Student{
public:
    Student(){					//默认构造函数,没有参数
        this->age = 20;
        this->num = 1000;
    };  
    Student(int a, int n):age(a), num(n){}; 	//初始化构造函数,有参数和参数列表
    Student(const Student& s){	//拷贝构造函数,这里与编译器生成的一致
        this->age = s.age;
        this->num = s.num;
    }; 
    Student(int r){   			//转换构造函数,形参是其他类型变量,且只有一个形参
        this->age = r;
        this->num = 1002;
    };
    ~Student(){}
public:
    int age;
    int num;
};

int main(){
    Student s1;
    Student s2(18,1001);
    int a = 10;
    Student s3(a);
    Student s4(s3);

    printf("s1 age:%d, num:%d\n", s1.age, s1.num);
    printf("s2 age:%d, num:%d\n", s2.age, s2.num);
    printf("s3 age:%d, num:%d\n", s3.age, s3.num);
    printf("s4 age:%d, num:%d\n", s4.age, s4.num);
    return 0;
}
//运行结果
//s1 age:20, num:1000
//s2 age:18, num:1001
//s3 age:10, num:1002
//s4 age:10, num:1002

调用拷贝构造函数的时机

用一个对象去初始化另一个对象的时候。

Student s5 = s1; 

函数的参数是对象时(非引用传递)。实现过程是,调用函数时先根据传入的实参产生临时对象,再用拷贝构造去初始化这个临时对象,在函数中与形参对应,函数调用结束后析构临时对象 。

void useClassS(Student s) {}
useClassS(s5);

函数的返回值是函数体内局部对象时 ,此时虽然发生(Named return Value优化)NRV优化,但是由于返回方式是值传递( 不是返回引用 ),所以会在返回值的地方调用拷贝构造函数。 理论的执行过程是: 产生临时对象,调用拷贝构造函数把返回对象拷贝给临时对象,函数执行完先析构局部变量,再析构临时对象, 依然会调用拷贝构造函数 .

Student getClassS()			// 发生拷贝构造函数的调用
{
    Student s;
    return s;
}
Student s6 = getClassS();

Student& getClassS2()			// 不发生拷贝构造函数的调用
{
    Student s;
    return s;
}
Student s7 = getClassS();
空类定义时生成的成员函数

参考【面试实战】C++中类会自动生成哪些函数: 类仅声明的话,编译器不会生成任何成员函数, 而是只会生成1个字节的占位符。
空类定义时会生成6个成员函数。当空类Empty定义一个对象e时,sizeof(e)仍是为1,但编译器会生成6个成员函数:缺省的构造函数,拷贝构造函数,析构函数,赋值运算符,两个取址运算符。即:

class Empty
{
  public:
    Empty();                            //缺省构造函数
    Empty(const Empty &rhs);            //拷贝构造函数
    ~Empty();                           //析构函数 
    Empty& operator=(const Empty &rhs); //赋值运算符
    Empty* operator&();                 //取址运算符
    const Empty* operator&() const;     //取址运算符(const版本)
};

测试:

class Empty
{
};
int main()
{
	Empty *e = new Empty();							   //缺省构造函数
	cout << "sizeof(Empty):" << sizeof(Empty) << endl; // 1
	cout << "sizeof(e):" << sizeof(e) << endl;		   // 8
	delete e;										   //析构函数
	Empty e1;										   //缺省构造函数
	Empty e2(e1);									   //拷贝构造函数
	e2 = e1;										   //赋值运算符
	Empty *pe1 = &e1;								   //取址运算符(非const)
	const Empty *pe2 = &e2;							   //取址运算符(const)
	cout << "successfully excuated!" << endl;
	return 0;
}

输出:

sizeof(Empty):1
sizeof(e):8
successfully excuated!

区分初始化与赋值

对于简单类型来说,初始化和赋值没什么区别;对于类和复杂数据类型来说,这两者的区别就大了:

class A{
public:
    int num1;
    int num2;
public:
    A(int a=0, int b=0):num1(a),num2(b){};
    A(const A& a){};
    //重载 = 号操作符函数
    A& operator=(const A& a){
        num1 = a.num1 + 1;
        num2 = a.num2 + 1;
        return *this;
    };
};
int main(){

    A a(1,1);
    A a1 = a; //拷贝初始化操作,调用拷贝构造函数
    A b;
    b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
    return 0;
}

~析构函数

为什么析构函数一般写成虚函数? 由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。所以将析构函数声明为虚函数是十分必要的。在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。

#include <iostream>
using namespace std;

class Parent{
public:
    Parent(){
        cout << "Parent construct function"  << endl;
    };
    virtual ~Parent(){
        cout << "Parent destructor function" <<endl;
    }
};

class Son : public Parent{
public:
    Son(){
        cout << "Son construct function"  << endl;
    };
    ~Son(){
        cout << "Son destructor function" <<endl;
    }
};

int main()
{
    Parent* p = new Son();
    delete p;
    p = NULL;
    return 0;
}
//运行结果:
//Parent construct function
//Son construct function
//Son destructor function
//Parent destructor function

创建对象时优化:NRV

参考: 关于NRV优化 。 下面是一个Vector类,用于统计两个对象相加执行的构造函数调用次数。它具有默认构造函数和拷贝构造函数,并增加一个静态变量count用于统计构造函数调用次数:

class Vector
{
public:
    static int count;
 
    static void init()
    {
        count = 0;
    }
 
    int x,y;
 
    Vector()
    {
        x = 0;
        y = 0;
         
        // For analysis.
        count ++;
        printf("Default Constructor was called.[0x%08x]\n", this);
    }
 
    Vector(const Vector & ref)
    {
        x = ref.x;
        y = ref.y;
 
        // For analysis.
        count ++;
        printf("Copy Constructor was called.[copy from 0x%08x to 0x%08x].\n", &ref, this);
    }
 
};
 
int Vector::count = 0;

函数调用形式如下:

Vector a, b;
Vector::init();
printf("\n-- Test add() --\n");
Vector c = add(a, b);
printf("---- Constructors were called %d times. ----\n\n\n", Vector::count);

运行结果:

-- Test add() --
Default Constructor was called.[0x0012fef8]
Copy Constructor was called.[copy from 0x0012fef8 to 0x0012ff60].
---- Constructors were called 2 times. ----

因为此时没有优化,执行的伪代码如下(共有4个对象被创建):

Vector a, b;
Vector::init();
printf("\n-- Test add() --\n");
Vector __temp0;     // 构造函数.
add(__temp0, a, b);
Vector c(__temp0);  // 拷贝构造函数.
printf("---- Constructors were called %d times. ----\n\n\n", Vector::count);

如果使用NRV优化(cl /Ox),则执行的伪代码如下(共有3个对象被创建):

Vector c;
add(c, a, b);

也就是说,从a, b传入函数开始,一共创建了1个对象。

友元函数

有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。 友元函数可以。 友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,一 个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程 序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。 因此,尽量使用成员函数,除非不得已的情况下才使用友元函数。

  1. 普通函数,需要在类的定义中声明。 一个函数可以是多个类的友元函数,但是每个类中都要声明这个函数。
#include <iostream>
using namespace std;

class A
{
public:
 	friend void set_show(int x, A &a); //该函数是友元函数的声明
private:
 	int data;
};
void set_show(int x, A &a) //友元函数定义,为了访问类A中的成员
{
     a.data = x;
     cout << a.data << endl;
}
int main(void)
{
     class A a;
     set_show(1, a);
     return 0;
}
  1. 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和 保护成员)。 但是另一个类里面也要相应的进行声明。
#include <iostream>
using namespace std;
class A
{
public:
	friend class C; //这是友元类的声明
private:
 	int data;
};
class C //友元类定义,为了访问类A中的成员
{
public:
 	void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};
int main(void)
{
     class A a;
     class C c;
     c.set_show(1, a);
     return 0;
}

使用友元类时注意: (1) 友元关系不能被继承。 (2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否 有相应的声明。 (3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看 类中是否有相应的申明 。

什么时候使用友元函数:

1)运算符重载的某些场合需要使用友元。

2)两个类要共享数据的时候

初始化列表的优势

如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋 值,而初始化列表只做一次赋值操作。

#include <iostream>
using namespace std;
class A{
public:
	 int value;
	 A(){
	 	cout << "默认构造函数A()" << endl;
	 }
	 A(int a){
		 value = a;
		 cout << "A(int "<<value<<")" << endl;
	 }
	 A(const A& a){
		 value = a.value;
		 cout << "拷贝构造函数A(A& a): "<<value << endl;
	 }
};
class B
{
public:
	A a;
	A b;
	B() : a(1){
		b = A(2);
	}
};
int main(){
 	B b;
}
//输出结果:
//A(int 1)
//默认构造函数A()
//A(int 2)

从结果可以看出,在构造函数体内部初始化的对象b多了一次构造函数的调用过程,而对象a则没有。b经历了定义和赋值两步来完成初始化: 对象成员变量的定义动作发生在进入构造函数之前,因此进入构造函数后所做的事其实是一次赋值操作(对象已存在); 而初始化列表只做一次赋值操作。

编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何显示用户 代码前。list中的项目顺序是由类中的成员声明顺序决定的,不是初始化列表中的排列顺序决定的。

成员初始化列表会在什么时候用到?

  1. 当初始化一个引用成员变量时;

  2. 初始化一个 const成员变量时;

  3. 当调用一个基类的构造函数,而构造函数拥有一组参数时;

  4. 当调用一个成员类的构造函数,而他拥有一组参数;

成员初始化列表

#include<iostream>
#include<string>
using namespace std;
class Weapon
{
private:
    string name;
    const string type;
    const string model;
public:
    Weapon(string& name, string& type, string& model) :name(name), type(type), model(model)
    {
        name = "Cloud";
    }
    string getProfile()
    {
        return "name: " + name + "\ntype: " + type + "\nmodel: " + model;
    }
};

int main(void)
{
   string name = "Comet";	    
    string type = "carbine";	    
    string model = "rifle";	    
    Weapon weapon(name, type, model);
    cout << weapon.getProfile() << endl;
	
    cin.get();
    return 0;
}

注意观察,构造函数里的 name = “Cloud”; 被初始化列表的值覆盖了 .

type和model都是常量,可以初始化但不能赋值,如果试图在构造函数的函数体中进行如 type = “xxx”;之类的 赋值,将会报错。 从概念上讲,在进入构造函数的函数体之前,对象已经被创建,所以必须在对象创建之前完成初始化,所以C++发明了初始化列表,这种形式的赋值被认为就是初始化。 C++ 成员初始化列表

虚函数virtual

定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。它虚就虚在所谓"推迟联编"或者"动态联编"上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为"虚"函数。 虚函数只能借助于指针或者引用来达到多态的效果。 C++ 虚函数和纯虚函数的区别.

class A
{
public:
    virtual void foo()
    {
        cout<<"A::foo() is called"<<endl;
    }
};
class B:public A
{
public:
    void foo()
    {
        cout<<"B::foo() is called"<<endl;
    }
};
int main(void)
{
    A *a = new B();
    a->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
    return 0;
}

注意:虚函数必须实现吗?

虚函数必须实现 ; 父类和子类都有各自的版本, 由多态方式调用的时候动态绑定。

多态虚表

使用virtual修饰的函数为虚函数。含有虚函数的类,实例化为对象时,编译器会自动生成虚表,并且,对象地址的前四个字节存储指向虚表的指针 vptr。

#include <iostream>
using namespace std;

class Base{
public:
    virtual void fun(){
        cout << " Base::func()" <<endl;
    }
};

class Son1 : public Base{
public:
    virtual void fun() override{
        cout << " Son1::func()" <<endl;
    }
};

class Son2 : public Base{

};

int main()
{
    Base* base = new Son1;
    base->fun();
    base = new Son2;
    base->fun();
    delete base;
    base = NULL;
    return 0;
}
// 运行结果
// Son1::func()
// Base::func()

虚表是一个一维 数组,保存了虚函数的入口地址。 在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为 父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指 针,令它指向子类的虚表。 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类 的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面 。

重载/重写(实现)/隐藏

重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系

重载是指在同一范围定义中的(成员)函数,它们的函数名相同,但是参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。

重写指的是在派生类中覆盖基类中的同名虚函数,重写就是重写函数。要求两个函数的参数个数,类型,返回值类型都相同。

//父类
class A{
public:
    virtual int fun(int a){}
}
//子类
class B : public A{
public:
    //重写,一般加override可以确保是重写父类的函数
    virtual int fun(int a) override{}
}

隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数

情形1:父子函数名、参数都相同,但是基类中不是虚函数

//父类
class A{
public:
    void fun(int a){
        cout << "A中的fun函数" << endl;
    }
};
//子类
class B : public A{
public:
    //隐藏父类的fun函数
    void fun(int a){
        cout << "B中的fun函数" << endl;
    }
};
int main(){
    B b;
    b.fun(2); //调用的是B中的fun函数
    b.A::fun(2); //调用A中fun函数
    return 0;
}

情形2:父子仅函数名相同,也会隐藏

//父类
class A{
public:
    virtual void fun(int a){
        cout << "A中的fun函数" << endl;
    }
};
//子类
class B : public A{
public:
    //隐藏父类的fun函数
   virtual void fun(char* a){
       cout << "A中的fun函数" << endl;
   }
};
int main(){
    B b;
    b.fun(2);    //报错,调用的是B中的fun函数,参数类型不对
    b.A::fun(2); //调用A中fun函数
    return 0;
}

由上可知,即使隐藏,也可以使用子类对象.父类名::父类同名函数(参数)的方式进行调用
上面的隐藏的两个例子中,没有使用多态进行测试,即用父类指针指向子类对象。后者的测试,见后面的动态绑定小节。

纯虚函数(抽象类)

纯虚函数: 1、 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 为了解决上述问题,引入了纯虚函数的概念 。含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

在基类中实现纯虚函数的方法是在函数原型后加 =0:

virtual void funtion1()=0;

抽象类 : 带有纯虚函数的类为抽象类。 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

类型安全(多态引起)

#include<iostream>
using namespace std;

class Parent{};
class Child1 : public Parent
{
public:
    int i;
    Child1(int e):i(e){}
};
class Child2 : public Parent
{
public:
    double d;
    Child2(double e):d(e){}
};
int main()
{
    Child1 c1(5);
    Child2 c2(4.1);
    Parent* pp;
    Child1* pc1;

    pp=&c1; 
    pc1=(Child1*)pp;  	// 类型向下转换 强制转换,由于类型仍然为Child1*,不造成错误
    cout<<pc1->i<<endl; //输出:5

    pp=&c2;
    pc1=(Child1*)pp;   //强制转换,且类型发生变化,将造成错误
    cout<<pc1->i<<endl;// 输出:1717986918
    return 0;
}

动态绑定

静态类型:对象在声明时采用的类型,在编译期既已确定;

动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;

静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;

动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

  • 静态类型和动态绑定
class A
{
public:
 /*virtual*/ void func() { std::cout << "A::func()\n"; }
};
class B : public A
{
public:
 void func() { std::cout << "B::func()\n"; }
};
class C : public A
{
public:
 void func() { std::cout << "C::func()\n"; }
};
void test_static_band()
{
     C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*;
     B* pb = new B(); //pb的静态类型和动态类型也都是B*;
     A* pa = pc; //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;
     pa = pb; //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;
     C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;

     pa->func(); //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func();
     pc->func(); //C::func() pc的动、静态类型都是C*,因此调用C::func();
     pnull->func(); //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;
}

上面是没有 virtual 的情况,就是静态绑定. 每个变量,它们所对应的函数或属性都依赖于对象的定义时的类型(静态类型),在编译期就完成了.

  • 如果加上 virtual ,则变成动态绑定.输出结果如下:

B::func()
C::func()

再加一个异常输出,和main函数的非0返回值.

因此,要想实现多态,基类A中的func必须是virtual函数,进行动态绑定( 在继承体系中,只有虚函数使用的是动态绑定,其他的全部是静态绑定), 否则, 不论pa、pb、pc指向哪个子类对象,对func的调用都是在定义pa、pb、pc时的静态类型决定,早已在编译期确定了。

本文代码里都是针对指针的情况来分析的,但是对于引用的情况同样适用,保留上面的virtual,再测试下面的代码:

 cout<< "使用&来调用: A& qa"<<endl;
 C c;
 A& qa = c;
 qa.func();

输出如下:

使用&来调用: A& qa
C::func()

  • 注意:

绝对不要重新定义继承而来的非虚(non-virtual)函数,因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不 可预知的隐患和莫名其妙的BUG;

另外,在动态绑定也即在virtual函数中,要注意默认参数的使用。当缺省参数和virtual函数一起使用的时候一定要谨慎,不然出了问题怕是很难排查。

class E
{
public:
 virtual void func(int i = 0)
 {
 std::cout << "E::func()\t" << i << "\n";
 }
};
class F : public E
{
public:
 virtual void func(int i = 1)
 {
 std::cout << "F::func()\t" << i << "\n";
 }
};
void test2()
{
 F* pf = new F();
 E* pe = pf;
 pf->func(); //F::func() 1 正常,就该如此;
 pe->func(); //F::func() 0 哇哦,这是什么情况,调用了子类的函数,却使用了基类中参数的默认值!
}

设置类对象只能静态/动态分配

参考107、类如何实现只能静态分配和只能动态分配. 使用new运算符会将类对象会被建立在堆上,因此只要限制new运算符的权限,就可以实现类对象只能建立在堆/栈上.

为了实现类对象只能建立在栈上,可以将new运算符设为私有。

为了实现类对象只能建立在堆上,构造、析构函数设为protected属性,再用子类来动态创建

//只能动态分配
class A
{
protected:
    A(){}
    ~A(){}
public:
    static A* create(){return new A();}
    void destory(){delete this;}
};

//只能静态分配
class B
{
private:
    void* operator new(size_t t){}          //注意函数的第一个参数和返回值都是固定的
    void operator delete(void* ptr){}       //重载了new就需要重载delete
public:
    B(){}
    ~B(){}
};

int main()
{
    A* a = A::create();		// 限制了必须调用new
    B b;					// 限制没有调用new
    return 0;
}

模板

C++ 模板: 模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
c++中模板是什么?为什么要定义模板?.通常我们想要比较不同数据类型的时候不得不定义两种不同的函数来表示区分,即使两个函数的功能是完全一致的.为了能精简代码避免和强类型的冲突(强类型程序设计中,参与运算的所有对象的类型在编译时即确定下来,并且编译程序将进行严格的类型检查),我们就需要用到模板去改善这种情况。

函数模板

总的来说是: template <typename T> + 函数定义{函数体}
具体点:

template <typename 类型占位符T> 
返回值类型 函数名(参数列表)
{
   // 函数的主体
}

注:classtypename修饰的类型参数,代表一种类型,像是int,long,long long类型一样,再去声明一个变量。

使用函数模板的例子:

#include <iostream>
#include <string>
 
using namespace std;
 
template <typename T>
inline T const& Max (T const& a, T const& b) 
{ 
    return a < b ? b:a; 
} 
int main ()
{
 
    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl; 
 
    double f1 = 13.5; 
    double f2 = 20.7; 
    cout << "Max(f1, f2): " << Max(f1, f2) << endl; 
 
    string s1 = "Hello"; 
    string s2 = "World"; 
    cout << "Max(s1, s2): " << Max(s1, s2) << endl; 
 
    return 0;
}

函数模板也可以重载

template <class T>
T Max(T x, T y)
{
    return x > y ? x : y;
}

template <class T>
T Max(T x, T y, T z)
{
    return x > y ? (x > z ? x : z) : (y > z ? y : z);
}

void test_template_2()
{
    cout << Max<int>(1, 2) << endl;    //调用实例化的Max(int,int)
    cout << Max('A', '8') << endl;            //隐式实例化char型,生成char型模板函数
    cout << Max<int>(1, 2, 3) << endl;    //调用实例化的Max(int,int,int)
    cout << Max<double>(3.0, 2.9) << endl;    //显示实例化为double型,生成double型模板函数
}

输出的结果是:2,A,3,3

显式实例化,可以像Max<int>(1, 2)一样,用<int>指出参数的类型,当然也可以不这样,例如Max(1, 2)也行.

还可以用模板进行两种类型数据的比较:

#include<iostream>
using namespace std;
template<typename type1,typename type2>//函数模板
type1 Max(type1 a,type2 b)
{
 return a > b ? a : b;
}
void main()
 {
 cout<<"Max = "<<Max(5.5,'a')<<endl;
} 

类模板

形式是:

template <class 类型占位符T> 
class 类名 {
//类的定义
}

一个例子:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
 
using namespace std;
 
template <class T>
class Stack {
  private:
    vector<T> elems;     // 元素

  public:
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty();
    }
};
// 实现类成员函数
template <class T>
void Stack<T>::push (T const& elem)
{
    // 追加传入元素的副本
    elems.push_back(elem);
}

template <class T>
void Stack<T>::pop ()
{
    if (elems.empty()) {
        throw out_of_range("Stack<>::pop(): empty stack");
    }
    // 删除最后一个元素
    elems.pop_back();
}

template <class T>
T Stack<T>::top () const
{
    if (elems.empty()) {
        throw out_of_range("Stack<>::top(): empty stack");
    }
    // 返回最后一个元素的副本
    return elems.back();
}
// 实例化2个类,分别是int类型的栈,和string类型的栈
void test_template_class()
{
    try {
        Stack<int>  intStack;  // int 类型的栈
        Stack<string> stringStack;    // string 类型的栈

        // 操作 int 类型的栈
        intStack.push(7);
        cout << intStack.top() <<endl;

        // 操作 string 类型的栈
        stringStack.push("hello");
        cout << stringStack.top() << endl;
        stringStack.pop();
        cout << "第一次应该还没有异常吧!"<< endl;
        stringStack.pop();
    }
    catch (exception const& ex) {
        cerr << "Exception: " << ex.what() <<endl;
    }
}

[C++]STL中vector容器 begin()与end()函数、front()与back()的用法.输出:

7
hello
第一次应该还没有异常吧!
Exception: Stack<>::pop(): empty stack

注意:

定义特定类型的Stack对象: Stack<int> intStack; 形如模板类<类型>变量名

类模板和实现一般都是放在一个.h文件中

一般在头文件中放置全部的模板声明和定义。 因为模板仅在需要的时候才会实例化出来。template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。

模板声明与定义要放在同一文件中? 对C++编译器而言,当调用函数的时候,编译器只需要看到函数的声明。当定义类类型的对象时,编译器只需要知道类的定义,而不需要知道类的实现代码。因此,应该将类的定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。 但在处理模板函数和类模板时,问题发生了变化:要进行实例化模板函数和类模板,要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?

Template<typename T> 
class vector {
public:
	typedef T   value_type;
	typedef value_tyep* iterator;
	// ...
}

基础知识点

字符串

const char* 与string

string 是c++标准库里面其中一个,封装了对常量字符串的操作,实际操作过程我们可以用const char*(常量指针)给 string类初始化.

// string转const char*
string s = “abc”;
const char* c_s = s.c_str();

// const char* 转string,直接赋值即可
const char* c_s = “abc”;
string s(c_s);

string是常量字符串,char*是可变字符串,两者之间的转换如下:

// string 转char*
string s = “abc”;
char* c;
const int len = s.length();
c = new char[len+1];
strcpy(c,s.c_str());

// char* 转string
char* c = “abc”;
string s(c);

类似的,有下面的const char* 转char*

// const char* 转char*
const char* cpc = “abc”;
char* pc = new char[strlen(cpc)+1];
strcpy(pc,cpc);

// char* 转const char*,直接赋值即可
char* pc = “abc”;
const char* cpc = pc;
sprintf,strcpy与strncpy函数
int sprintf(char *str, const char *format, ...)

sprintf主要实现其他数据类型格式到字符串的转化,str为目标字符号串数组,后面的参数与printf相同。

#include <math.h>
char str[80];
sprintf(str, "Pi 的值 = %f", M_PI);

下面这两个函数主要实现字符串变量间的拷贝:

char* strcpy(char* strDest, const char* strSrc)
char* strncpy(char* strDest, const char* strSrc, int pos)

strcpy函数: 如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况。因此可以用strncpy()来取代。

strncpy函数可用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠。

指针

指针加减
long long getPnum(int * p){	// 该函数可以得到int*指针的10进制数
    return (long long) p;
}
void test_pointer_minus_add()
{
     int *a, *b, c;
     a = (int*)0x500;
     b = (int*)0x520;
     c = b - a;
     cout<< c << endl; // 8

     long long aN = getPnum(a);
     cout<< "((long long)b-(long long)a)/4 :  "<< (getPnum(b)- aN)/4 << endl;

     a += 0x020;
     c = b - a;
     cout<< c << endl; // -24
     cout<< "((long long)b-((long long)a + 32*4))/4 :\n" << (getPnum(b)-(aN + 32*4))/4 << endl;
}

a = (int*)0x500;实际上进行了运算: 地址为:5*16^2 = 1280,即(long long) a为1280.

c = b - a;则除以了类型, 也就是说a和b所指向的地址之间间隔32个位,但是 考虑到是int类型占4位,所以c的值为32/4=8.

从上面a += 0x020;中实际加了2*16*4可知,指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转 成10进制计算,计算结果除以类型长度取得结果;

数组指针

定义数组int a[10]; 那么&a是数组的指针,其类型为int (*)[10];
此时如果int (*p)[10] = &a;,那么p的类型是int (*)[10];
即: p是一个指针,指向的对象区域是装有10个int的数组;
那么,既然p是指针

*p == a 但是注意 (*p)[1] == a[1]而不是*p[1]
p[1] == *(p + 1) ,是一个位置,这个位置比p偏移了10个int的大小.理解这个等式可以类比a[1] == *(a+1),但是区别在于: a是数组首元素地址(单个int),而p是数组的地址(整个数组)

void test_array_pointer(){
    int a[10] = {1,2,3};
    int (*p)[10] = &a;
    int * p2 = (int *) p;

    cout << a << endl;      // 0x63fdc0
    cout << a[1] << endl;   // 2
    cout << *(a+1) << endl; //2  *(a + 1) == a[1]

    cout << *p << endl;     // 0x63fdc0
    cout << *p[1]<<endl;    // 0
    cout << (*p)[1]<<endl;  // 2,这是正确的使用p访问数组元素的方式

    cout << *(p + 1)<<endl; // 0x63fde8     (e-c)*16+8=40,表示数组a的长度,说明偏移了一个数组

    cout << *p[1]<< "," << p[1] <<endl;    // 0,0x63fde8     *p[1] != (*p)[1]
    cout << *p[2]<<endl;    // 访问已越界,出现的结果在变化



    cout << *(p2) << endl;      // 1
    cout << *(p2+1) << endl;    // 2
}

输出:

0x63fdc0
2
2
0x63fdc0
6553024
2
0x63fde8
6553024,0x63fde8
14751472
1
2

引用作为返回值

函数内创建的变量,尽量不要返回。如果是局部变量,上分配的返回后就会回收,上new出的虽然不存在局部变量的被动销毁问题,但是如果函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。

下面两种情况可以:

  1. 可以返回类成员的引用,但是最好加const。否则其他对象可能会修改该属性,破坏业务规则的完整性。
  2. 局部变量声明为局部静态变量时,允许返回局部变量的引用。因为这样就人为地把局部变量的生存周期改成了与整个源程序相同。所以这种方式返回局部变量的引用是安全的。
const Mapping& Test::mapping() const         //返回的是基类引用
{
  if(condition1)
  {
    static MappingQ1 m;                      //这里是个派生类对象
    return m;
  }
  else if(condition2)
  {
    static MappingQ2 m;                      //另一个派生类对象
    return m;
  }
}

左值引用右值引用

c++ 左值引用与右值引用, 左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用; 但使用常引用后,数据只读了,因为其被const修饰成常量引用了。

int& var = 10;   // 错.无法对一个立即数10取地址,因为立即数并没有在内存中存储,而是存储在寄存器中

const int &var = 10;  // 可. 使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10

// 与上面一行等价
const int temp = 10; 
const int &var = temp;

C++中,可以取地址的,有名字的,非临时的就是左值(常见变量,函数返回的引用,const对象等);不能取地址的,没有名字的,临时的就是右值(如立即数,函数返回的值 ); 从本质上理解,程序员只能确保在本行代码有效的,创建和销毁由编译器幕后控制,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。

右值引用是C++ 11新增的特性,右值引用用来绑定到右值,右值的生存期会延长至与绑定的右值引用的生存期。 在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是, 右值引用可以进行读写操作,而常引用只能进行读操作。

右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。

int &&var = 10;		// 右值引用

参考博文中有一个使用右值引用实现简单的顺序栈的例子.定义了一个转移构造函数.

class Stack
{
private:
    int *mpstack;
    int mtop;
    int msize;
    
public:
    Stack(int size = 1000) :msize(size), mtop(0){
		cout << "Stack(int)" << endl;
		mpstack = new int[size];
    }
    ~Stack(){ // 析构
        if (mpstack != nullptr){
            delete[]mpstack;
            mpstack = nullptr;
        }
    }
	// 带右值引用参数的转移构造函数
    Stack(Stack &&src):msize(src.msize), mtop(src.mtop) {
        cout << "Stack(Stack&&)" << endl;
        /*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
        mpstack = src.mpstack;  
        src.mpstack = nullptr;
    }
}

拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另 一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为 NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收 a->value指向的空间;

比较(结构体,指针,类)

  • 可以通过重载 “==” 操作符来做.
struct foo {
 int a;
 int b;
 bool operator==(const foo& rhs) *//* *操作运算符重载*
 {
 return( a == rhs.a) && (b == rhs.b);
 }
};
  • 指针直接比较,如果保存的是同一个实例(指向的对象)地址,则(p1==p2)为真;
  • 类的与结构体的差不多,如下
class Student{
	private:
		string name;
		int age;
	public:
		//运算符的重载
		bool operator== (const Student &s) const{
			return this->name == s.name && this->age == s.age;
		}

		//或者也可用友元函数重载等于运算符
		// bool operator== (const Student &s,const Student&s1){
		//	 return (s.age == s1.age && s.name == s1.name && s.score == s1.score);
		// } 
};

条件编译 ifdef-endif

一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。 条件编译形式为

# ifdef 标识符 
程序段1 
#else 程序段2 
#endif 

上面代码中,当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译 程序段2。 或:

#ifdef
程序段1
#endif

在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。 在头文件中使用#define(示例见关键字/运算符部分)、#ifndef、#ifdef、#endif能避免头文件重定义。

结构体内存对齐

结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。

未特殊说明时:

分配内存的顺序是按照声明的顺序;

每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏 移量是整数倍为止。

整个结构体按结构体中size最大的成员对齐,或者说, 最后整个结构体的大小必须是里面变量类型最大值的整数倍。(若有double成员,按8字节对齐。)

指定对齐方式

c++11以后引入两个关键字 alignasalignof。其中alignof可以计算出类型的对齐方式,alignas可以指定结构体的对齐方式,但若要成功指定,必须不小于自然对齐的最小单位。

// 一般情况
struct Info {
 uint8_t a;
 uint16_t b;
 uint8_t c;
};
std::cout << sizeof(Info) << std::endl; // 6 2 + 2 + 2
std::cout << alignof(Info) << std::endl; // 2

// alignas 生效的情况
struct alignas(4) Info2 {
 uint8_t a;
 uint16_t b;
 uint8_t c;
};
std::cout << sizeof(Info2) << std::endl; // 8 4 + 4
std::cout << alignof(Info2) << std::endl; // 4

// alignas 未生效的情况
struct alignas(1) Info2 {
 uint8_t a;
 uint16_t b;
 uint8_t c;
};
std::cout << sizeof(Info2) << std::endl; // 6 2 + 2 + 2
std::cout << alignof(Info2) << std::endl; // 2
单字节对齐

如果想使用单字节对齐的方式,使用alignas是无效的。应该使用#pragma pack(push,1) (Visual Studio环境、Keil环境)或者使用__attribute__((packed))(Linux环境gcc)。参考单字节对齐

#if defined(__GNUC__) || defined(__GNUG__)
 #define ONEBYTE_ALIGN __attribute__((packed))
#elif defined(_MSC_VER)
 #define ONEBYTE_ALIGN
 #pragma pack(push,1)
#endif
struct Info {
 uint8_t a;
 uint32_t b;
 uint8_t c;
} ONEBYTE_ALIGN;
#if defined(__GNUC__) || defined(__GNUG__)
 #undef ONEBYTE_ALIGN
#elif defined(_MSC_VER)
 #pragma pack(pop)
 #undef ONEBYTE_ALIGN
#endif
std::cout << sizeof(Info) << std::endl; // 6 1 + 4 + 1
std::cout << alignof(Info) << std::endl; // 6

指定4字节对齐,可以使用 #pragma pack(4) .

确定结构体中每个元素大小

添加了#pragma pack(n)后规则就变成了下面这样:

1、 偏移量要是n和当前变量大小中较小值的整数倍

2、 整体大小要是n和最大变量大小中较小值的整数倍

3、 n值必须为1,2,4,8…,为其他值时就按照默认的分配规则

我的是win10平台,64 位操作系统, 基于 x64 的处理器.下面的测试结果也是基于此.

#if defined(_MSC_VER)
 #define ONEBYTE_ALIGN
 #pragma pack(push,1)
#endif

struct Info {
 uint16_t a : 1;
 uint16_t b : 2;
 uint16_t c : 3;
 uint16_t d : 2;
 uint16_t e : 1;
 uint16_t pad : 7;
} ONEBYTE_ALIGN;
#if defined(_MSC_VER)
 #pragma pack(pop)
 #undef ONEBYTE_ALIGN
#endif
std::cout << sizeof(Info) << std::endl; 	// 2
std::cout << alignof(Info) << std::endl; 	// 2
获得结构成员相对于结构开头的字节偏移量
struct S
{
     int x;
     char y;
     int z;
     double a;
};
int main()
{
     cout << offsetof(S, x) << endl; // 0
     cout << offsetof(S, y) << endl; // 4
     cout << offsetof(S, z) << endl; // 8
     cout << offsetof(S, a) << endl; // 12
     return 0;
}

在 Visual Studio 2019 + Win10 下 , 因为 double是8字节,需要找一个8的 倍数对齐, 那么最后一个就是16. 当然了,如果加上 #pragma pack(4) 指定4字节对齐方式就可以了。

在这里插入图片描述

一些关键字/运算符

const常量折叠

const常量的内存分配区是很普通的栈或者全局区域。也就是说const常量只是编译器在编译的时候做检查,根本不存在什么read-only的区域。 C++的常量折叠(一)里还与 预编译指令(#define) 进行了对比。

int main()
{
    int i0 = 11;

    const int i = 0;         //定义常量i
    int *j = (int *) &i;   //看到这里能对i进行取值,判断i必然后自己的内存空间
    *j = 1;                  //对j指向的内存进行修改
    printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j); //观看实验效果
    // &i,j的地址相同;i=0,(*j)=1
    
    const int ck = 9;     //这个对照实验是为了观察,对常量ck的引用时,会产生的效果
    int ik = ck;

    int i1 = 5;           //这个对照实验是为了区别,对常量和变量的引用有什么区别
    int i2 = i1;

    return 0;
}

image

i是可折叠常量,在编译阶段对i的引用已经别替换为i的值了,同时不同于宏替换的是,这个i还被存到了常量表中。 同时在常量表中也有i这个变量,不然的话对i取地址是不合法的,这是和宏替换的不同点,宏替换是不会把宏名称放到常量表中的,预编译完就用不到了。 也就是说:

printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j);

中的i在预处理阶段已经被替换,其实已经被改为:

printf("0x%p\n0x%p\n%d\n%d\n",&i,j,0,*j);

const修饰函数(与 mutable )

不想让某个函数修改成员变量的值,那么也可以把这个函数声明成const成员函数。这样的作用主要是为了保护数据成员。 const在函数中间的作用

class A
 {
 public:
     void area(int x,int y){length=x;width=y;}
     void print(){cout<<"两数相乘为:"<<length*width<<endl;}
 
 private:
     int length;
     int width;
 };

对于print()函数,我们发现,其实它的作用只是把length和width的乘积打印到屏幕上,并不会修改类A里面的私有变量length和width的值,针对这一点,我们就可以把print()函数修饰成为const成员函数,即

void print()const{cout<<"两数相乘为:"<<length*width<<endl;}

但是, mutable则是为了能够突破const的封锁线。 const成员函数和mutable关键字 被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改,这使得类的一些次要的或者辅助性的成员变量随时可以被更改。 上面的还可以改为:

class A{
 public:
     void print() const {length *= 100; cout<<"两数相乘为:"<<length*width<<endl;} 
 private:
     mutable int length;
     int width;
 };

const_cast , dynamic_cast

#include <bits/stdc++.h>
using namespace std;
class Base{
public:
 Base() :b(1) {}
 virtual void fun() {};
 int b;
};
class Son : public Base{
public:
 Son() :d(2) {}
 int d;
};
int main(){
 int n = 97;
 //reinterpret_cast
 int *p = &n;
 //以下两者效果相同
 char *c = reinterpret_cast<char*> (p);
 char *c2 = (char*)(p);
 cout << "reinterpret_cast输出:"<< *c2 << endl;
 //const_cast
 const int *p2 = &n;
 int *p3 = const_cast<int*>(p2);
 *p3 = 100;
 cout << "const_cast输出:" << *p3 << endl;

 Base* b1 = new Son;
 Base* b2 = new Base;
 //static_cast
 Son* s1 = static_cast<Son*>(b1); //同类型转换
 Son* s2 = static_cast<Son*>(b2); //下行转换,不安全
 cout << "static_cast输出:"<< endl;
 cout << s1->d << endl;
 cout << s2->d << endl; //下行转换,原先父对象没有d成员,输出垃圾值
 //dynamic_cast
 Son* s3 = dynamic_cast<Son*>(b1); //同类型转换
 Son* s4 = dynamic_cast<Son*>(b2); //下行转换,安全
 cout << "dynamic_cast输出:" << endl;
 cout << s3->d << endl;
 if(s4 == nullptr)
 	cout << "s4指针为nullptr" << endl;
 else
     cout << s4->d << endl;


 return 0;
}
//输出结果
//reinterpret_cast输出:a
//const_cast输出:100
//static_cast输出:
//2
//-33686019
//dynamic_cast输出:
//2
//s4指针为nullptr

reinterpret_cast <转换的目标类型> (原来的指针/引用/算术类型/函数指针或成员指针) , 以下两者效果相同 :

char *c = reinterpret_cast<char*> (p);
char *c2 = (char*)(p);

const_cast <不含const/volatile修饰的相同类型>(const/volatile修饰的类型的原变量) ,类型可以是指针,引用; const_cast一般用于修改底指针。如const char *p形式 .

int n = 97;
const int *p2 = &n;
// int *p3 = p2;	error: invalid conversion from 'const int*' to 'int*' [-fpermissive]
int *p3 = const_cast<int*>(p2);

static_cast < 基本类型,void类型等或父子类 > (对应的基本类型,void类型等或父子类的变量). 注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。

Base* b2 = new Base;
Son* s2 = static_cast<Son*>(b2); //下行转换,不安全
cout << s2->d << endl; 			//下行转换,原先父对象没有d成员,输出垃圾值

dynamic_cast < 类的指针、类的引用或者void* > (对应的类的指针、类的引用或者void*). dynamic_cast运算符可以在执行期决定真正的类型,也就是说expression必须是多态类型。如果下行转换是安全的(也就说,如果基类指针或者引用确实指向目标派生类对象)这个运算符会传回适当转型过的指针。如果下行转换不安全,这个运算符会传回空指针 nullptr (也就是说,基类指针或者引用没有指向该派生类对象).

Son* s4 = dynamic_cast<Son*>(b2); //下行转换,安全
if(s4 == nullptr)
    cout << "s4指针为nullptr" << endl;
else
    cout << s4->d << endl;

volatile

const常量指针与指针常量:

const读成常量,*读成指针。const在前*在后,顺着读就是常量指针,const在后,*在前就读成指针常量
常量指针和指针常量的区别

当使用 volatile 声明的变量的值的时候, 系统总是重新从它所在的内存读取数据, 而不是读寄存器内的备份。 这是因为用volatile声明的变量可能被某些编译器未知的因素更改, 比如:操作系统、硬件或者其它线程等。
多线程中被几个任务共享的变量需要定义为volatile类型。当两个线程都要用到某一个变量且该变量的值会被改变时,如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。 此时,应该用volatile声明,防止优化编译器把变量从内存装入CPU寄存器中。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
volatile用在如下的几个地方(得结合计组来理解):

  1. 中断服务程序中修改的供其它程序检测的变量需要加volatile;
  2. 多任务环境下各任务间共享的标志应该加volatile;
  3. 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;

volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念。

修饰指向的内容(对象、数据):

const char* cpch;
volatile char* vpch;

修饰指针自身

char* const pchc;
char* volatile pchv;

volatile类似于大家所熟知的const也是一个类型修饰符。volatile是给编译器的指示来说明对它所修饰的对象不应该执行优化。volatile的作用就是用来进行多线程编程。在单线程中那就是只能起到限制编译器优化的作用。详解volatile在C++中的作用,谈谈C++的volatile关键字以及常见的误解

可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。

除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。

?? C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatile向const一样会从类传递到它的成员。

explicit关键字

首先, C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显式的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式). explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了.

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

“CxString string2 = 10;” 这句为什么是可以的呢? 在C++中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象. 也就是说 “CxString string2 = 10;” 但是, 上面的代码中的_size代表的是字符串内存分配的大小, 那么调用的第二句 “CxString string2 = 10;” 和第六句 “CxString string6 = ‘c’;” 就显得不伦不类, 而且容易让人疑惑. 有什么办法阻止这种用法呢? 答案就是使用explicit关键字. C++ explicit关键字详解

explicit CxString(int size)  
    {  
        _size = size;  
        // 代码同上, 省略...  
    }  

extern

[extern “C”] 在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。如: extern "C" void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$也可能是别的,这要看编译器的"脾气"了(不同的编译器采用的方法不一样),为什么这么做呢,因为C++支持函数的重载啊! 下面是一个标准的写法: C/C++中extern关键字详解

//在.h文件的头上
#ifdef __cplusplus
#if __cplusplus
extern "C"{
 #endif
 #endif /* __cplusplus */
 …c的函数声明1
 …c的函数声明2
 //.h文件结束的地方
 #ifdef __cplusplus
 #if __cplusplus
}
#endif
#endif /* __cplusplus */ 

还可参考“#ifdef __cplusplus extern “C” { #endif”的定义:假设某个函数的原型为: void foo( int x, int y ); 该函数被C编译器编译后在符号库中的名字可能为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不 同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。下面以例子说明,如何 在C++中使用C的函数,或者在C中使用C++的函数。

C++引用C函数的例子:

//test.c
#include <stdio.h>
void mytest()
{
	printf("mytest in .c file ok\n");
}

//main.cpp
extern "C"
{
	void mytest();
}
int main()
{
    mytest();
    return 0;
}
通用用法

通用用法:C++使用C

#ifdef USE_C
extern "C"{
#endif
/*写c函数声明*/
#ifdef USE_C
}
#endif

C使用C++

#ifdef USE_CPP
#include<iostream>
using namespace std;
extern "C"{
#endif
/*写c函数声明*/
#ifdef USE_CPP
}
#endif

这样,可以将mytest()的实现放在.c或者.cpp文件中,可以在.c或者.cpp文件中include "test.h"后使用头文件里面的函数,而不会出现编译错误。

声明全局变量

[不是 extern “C” ] extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。 记住它是一个声明不是定义!

在头文件test1.h中有下列声明:

#ifndef TEST1H
#define TEST1H
extern char g_str[]; // 声明全局变量g_str
void fun1();
#endif

test1.cpp

#include "test1.h"
char g_str[] = "123456"; // 定义全局变量g_str
void fun1() { cout << g_str << endl; } 

以上是test1模块, 它的编译和连接都可以通过,如果我们还有test2模块也想使用g_str,只需要在原文件中引用就可以了. 在test2.cpp

#include "test1.h"
void fun2()  { cout << g_str << endl;  }

以上test1和test2可以同时编译连接通过,如果你感兴趣的话可以用ultraEdit打开test1.obj,你可以在里面找到"123456"这个字符串,但是你却不能在test2.obj里面找到,这是因为g_str是整个工程的全局变量,在内存中只存在一份,test2.obj这个编译单元不需要再有一份了,不然会在连接时报告重复定义这个错误!

inline:内联函数(比较#define)

在c中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。 宏虽然看起来像一个函数调用,但是会有隐藏一些难以发现的错误(下面两个例子)。第二个问题是c++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类的成员函数为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function).内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。C++内联函数(inline function).

预处理宏和内联函数的一些比较如下:

#define ADD(x,y) x+y
inline int Add(int x,int y){
    return x + y;
}
void test(){
    int ret1 = ADD(10, 20) * 10; //希望的结果是300
    int ret2 = Add(10, 20) * 10; //希望结果也是300
    cout << "ret1:" << ret1 << endl; //210
    cout << "ret2:" << ret2 << endl; //300
}
#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x,int y){
    return x < y ? x : y;
}
void test02(){
    int a = 1;
    int b = 3;
    //cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3
    cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2
}

此外: 预定义宏函数没有作用域概念,无法作为一个类的成员函数,也就是说预定义宏没有办法表示类的范围。

在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。

inline int func(int a){return ++;}

编译器会将函数类型(包括函数名字,参数类型,返回值类型)放入到符号表中。同样,也会将内联函数放入符号表。当调用一个内联函数的时候,编译器首先确保传入参数类型是正确匹配的,或者如果类型不完全匹配,但是可以将其转换为正确类型,并且返回值在目标表达式里匹配正确类型,或者可以转换为目标类型,内联函数就会直接替换函数调用,这就消除了函数调用的开销。假如内联函数是成员函数,对象this指针也会被放入合适位置。内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间

内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为,编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。 唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。

  • 使用宏定义的地方都可以使用inline函数

类内部的内联函数: 但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数,不须在函数定义前面放一个inline关键字 。但是构造函数和析构函数声明为内联函数是没有意义的, 因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。

inline同register一样,只是个建议,编译器并不一定真正的内联。 有几种情况下编译器不会内联编译:

不能存在任何形式的循环语句

不能存在过多的条件判断语句

函数体不能过于庞大

不能对函数进行取址操作

auto,decltype和decltype(auto)的用法

auto:和原来那些只对应某种特定的类型说明符(例如 int)不同,auto 让编译器通过初始值来进行类型推演,从而获得定义变量的类型。所以说 auto 定义的变量必须有初始值。

//普通;类型
int a = 1, b = 3;
auto c = a + b;// c为int型

//const类型
const int i = 5;
auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int
auto k = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const int*
const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前面加上cosnt

//引用和指针类型
int x = 2;
int& y = x;
auto z = y; //z是int型不是int& 型
auto& p1 = y; //p1是int&型
auto p2 = &x; //p2是指针类型int*

decltype :有的时候我们还会遇到这种情况,我们希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就无力了,所以C++11又引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。

int func() {return 0};

//普通类型
decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调用函数func()
int a = 0;
decltype(a) b = 4; // a的类型是int, 所以b的类型也是int

//不论是顶层const还是底层const, decltype都会保留   
const int c = 3;
decltype(c) d = c; // d的类型和c是一样的, 都是顶层const
int e = 4;
const int* f = &e; // f是底层const
decltype(f) g = f; // g也是底层const

//引用与指针类型
//1. 如果表达式是引用类型, 那么decltype的类型也是引用
const int i = 3, &j = i;
decltype(j) k = 5; // k的类型是 const int&

//2. 如果表达式是引用类型, 但是想要得到这个引用所指向的类型, 需要修改表达式:
int i = 3, &r = i;
decltype(r + 0) t = 5; // 此时是int类型

//3. 对指针的解引用操作返回的是引用类型
int i = 3, j = 6, *p = &i;
decltype(*p) c = j; // c是int&类型, c和j绑定在一起

//4. 如果一个表达式的类型不是引用, 但是我们需要推断出引用, 那么可以加上一对括号, 就变成了引用类型了
int i = 3;
decltype((i)) j = i; // 此时j的类型是int&类型, j和i绑定在了一起

decltype(auto): 可以用来声明变量以及指示函数返回类型。在使用时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。

int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e

new

  • 普通的new,在C++中定义如下:
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();

因此在空间分配失败的情况下,抛出异常std::bad_alloc,而不是NULL:

#include <iostream>
#include <string>
using namespace std;
int main()
{
    try
    {
        char *p = new char[10e11];
        delete p;
    }
    catch (const std::bad_alloc &ex)
    {
        cout << ex.what() << endl;
    }
    return 0;
}
//执行结果:bad allocation
  • nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL:
#include <iostream>
#include <string>
using namespace std;

int main()
{
    char *p = new(nothrow) char[10e11];
    if (p == NULL) 
    {
        cout << "alloc failed" << endl;
    }
    delete p;
    return 0;
}
//运行结果:alloc failed
  • placement new 在一块已经分配成功的内存上重新构造对象或对象数组

定义如下:

void* operator new(size_t,void*);
void operator delete(void*,void*);

构造起来的对象或数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组的大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。

#include <iostream>
#include <string>
using namespace std;
class ADT{
    int i;
    int j;
public:
    ADT(){
        i = 10;
        j = 100;
        cout << "ADT construct i=" << i << "j="<<j <<endl;
    }
    ~ADT(){
        cout << "ADT destruct" << endl;
    }
};
int main()
{
    char *p = new(nothrow) char[sizeof ADT + 1];
    if (p == NULL) {
        cout << "alloc failed" << endl;
    }
    ADT *q = new(p) ADT;  //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可
    //delete q;//错误!不能在此处调用delete q;
    q->ADT::~ADT();//显示调用析构函数
    delete[] p;
    return 0;
}
//输出结果:
//ADT construct i=10 j=100
//ADT destruct

delete

C++中的delete和delete[ ]的区别: 使用new申请的内存,释放时用delete,使用new [ ]申请的内存释放时要用delete [ ]才行.

  • new,new[]

对于基本数据类型及其数组,new会调用operator new分配内存; 对于复杂类型对象,new 还会调用构造函数;

对于复杂类型数组,new[] 先调用operator new[]分配内存,然后在p的前4个高位字节写入数组大小n ( 存储数组大小),然后调用n次构造函数,最后返回一个指向该对象的指针。

  • delete,delete[]

delete简单数据类型默认只是调用free函数;对于复杂数据类型,会先调用析构函数再调用operator delete;

对于复杂类型数组, 实际分配的内存地址为[p-4].直接使用 而delete p会直接释放p指向的内存,造成崩溃;而delete[] p实际释放的就是p-4指向的内存。delete [] 取出保存在地址高位的表示对象个数的数,就知道了需要调用析构函数多少次了。

对于基本数据类型, int *a = new int[10]; 释放内存时用delete a;delete [ ] a; 都可以的.

但对于自定义数据类型(或一些类),包含析构函数,如果通过new申请了一个对象数组, 当使用delete时,仅仅调用了对象数组中第一个对象的析构函数,而使用delete [ ]的话,将会逐个调用析构函数。

#include <iostream>;
using namespace std;
 
class T {
public:
  T() { cout << "constructor" << endl; }
  ~T() { cout << "destructor" << endl; }
};
 
int main()
{
  const int NUM = 3;
 
  T* p1 = new T[NUM];
  cout << hex << p1 << endl;    // 输出P1的地址
  //  delete[] p1;
  delete p1;					// 调用1次析构函数
 
  cout << endl;
 
  T* p2 = new T[NUM];
  cout << p2 << endl;           
  delete[] p2;					// 调用3次析构函数
 
  return 0;
}

如果析构函数还有其他的操作,则会造成潜在的危险。

使用new来创建动态数组待验证一点:

new动态数组返回的并不是数组类型,而是一个元素类型的指针;

sizeof(比较strlen)

sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得(所以不能用来得到动态分配(运行时分配)存储空间的大小);参数可以是任何数据的类型或者数据(sizeof参数不退化).结果是变量的大小:字节个数.
而strlen是字符处理的库函数。strlen的参数只能是字符指针且结尾是’\0’的字符串。得到的结果是字符个数.

void test_sizeof_strlen(){

 const char* str = "name is Weber";
 cout << sizeof(str) << endl; // 取的是指针str的长度,是8
 cout << strlen(str) << endl; // 取的是这个字符串的长度,不包含结尾的 \0。大小是13

 int i = 50;
 cout << sizeof(i) << endl; // 取的是指针str的长度,是8

 int* pi = &i;
 cout << sizeof(pi) << endl; // 取的是指针str的长度,是8
}

输出:

8
13
4
8

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值