Skip to content

Latest commit

 

History

History
1296 lines (1043 loc) · 52.3 KB

Answer_C++.md

File metadata and controls

1296 lines (1043 loc) · 52.3 KB

C++疑惑解析

一.C++构造函数详解

1. 构造函数是干什么的

class Counter
{
public:
    // 类Counter的构造函数
    // 特点:以类名作为函数名,无返回类型
    Counter()
    {
        m_value = 0;
    }         
private:     
    // 数据成员
  int m_value;
}

该类对象被创建时,编译系统对象分配内存空间,并自动调用该构造函数->由构造函数完成成员的初始化工作;

定义类对象: Counter c1;

编译系统为对象 c1 的每个数据成员(m_value)分配内存空间,并调用构造函数Counter()自动地初始化对象 c1 的m_value值设置为0

故:构造函数的作用:初始化对象的数据成员。

2. 构造函数的种类

class Complex 
{         
private :
    double m_real;
    double m_imag;
public:
    // 无参数构造函数
    // 如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做
    // 只要你写了一个下面的某一种构造函数,系统就不会再自动生成这样一个默认的构造函数,如果希望有一个这样的无参构造函数,则需要自己显示地写出来
    Complex()
    {
         m_real = 0.0;
         m_imag = 0.0;
    } 
    // 一般构造函数(也称重载构造函数)
    // 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
    // 例如:你还可以写一个 Complex( int num)的构造函数出来
    // 创建对象时根据传入的参数不同调用不同的构造函数
    Complex(double real, double imag)
    {
         m_real = real;
         m_imag = imag;         
     }
    // 复制构造函数(也称为拷贝构造函数)
    // 复制构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中
    // 若没有显示的写复制构造函数,则系统会默认创建一个复制构造函数,但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险,具体原因请查询有关 “浅拷贝” 、“深拷贝”的文章论述
    Complex(const Complex & c)
    {
        // 将对象c中的数据成员值复制过来
        m_real = c.m_real;
        m_img  = c.m_img;
    }            
    // 类型转换构造函数,根据一个指定的类型的对象创建一个本类的对象
    // 例如:下面将根据一个double类型的对象创建了一个Complex对象
    Complex::Complex(double r)
    {
        m_real = r;
        m_imag = 0.0;
    }
    // 等号运算符重载
    // 注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建
    // 若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作
    Complex &operator=(const Complex &rhs)
    {
        // 首先检测等号右边的是否就是左边的对象本身,若是本对象本身,则直接返回
        if ( this == &rhs ) 
        {
            return *this;
        }     
        // 复制等号右边的成员到左边的对象中
        this->m_real = rhs.m_real;
        this->m_imag = rhs.m_imag;
            
        // 把等号左边的对象再次传出
        // 目的是为了支持连等 eg:    a=b=c 系统首先运行 b=c
        // 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象)    
        return *this;
    }
};

下面使用上面定义的类对象来说明各个构造函数的用法:

void main()
{
    // 调用了无参构造函数,数据成员初值被赋为0.0
    Complex c1,c2;

    // 调用一般构造函数,数据成员初值被赋为指定值
    Complex c3(1.0,2.5);
    // 也可以使用下面的形式
    Complex c3 = Complex(1.0,2.5);
        
    // 把c3的数据成员的值赋值给c1
    // 由于c1已经事先被创建,故此处不会调用任何构造函数
    // 只会调用 = 号运算符重载函数
    c1 = c3;
        
    // 调用类型转换构造函数
    // 系统首先调用类型转换构造函数,将5.2创建为一个本类的临时对象,然后调用等号运算符重载,将该临时对象赋值给c2
    c2 = 5.2;
      
    // 调用拷贝构造函数( 有下面两种调用方式) 
    Complex c5(c2);
    Complex c4 = c2;  // 注意和 = 运算符重载区分,这里等号左边的对象不是事先已经创建,故需要调用拷贝构造函数,参数为c2       
    
    所以=与拷贝最重要的区别就是看:若被copy的对象如果已经存在那就是赋值,就是赋值的时候两个对象都存在,而拷贝的时候是用一个已存在的去复制产生一个新的(未存在的)
}

补充:转换构造函数

一个构造函数接收一个不同于其类类型的形参,可以视为将其形参转换成类的一个对象。像这样的构造函数称为转换构造函数。

除了创建类对象之外,转换构造函数还为编译器提供了执行隐式类型转换的方法。只要在需要类的类型值的地方,给定构造函数的形参类型的值,就将由编译器执行这种类型的转换。

举一个简单的例子,先来看下面这个类:

class IntClass
{
 private:
     int value;
 public:
     //转换int的转换构造函数
     IntClass(int intValue)
     {
         value = intValue;
     }
     int getValue() const { return value; }
};

由于构造函数 IntClass(int) 只接收一个类型不同于 IntClass 的单个形参,所以它是一个转换构造函数。

只要上下文需要类对象,但提供的却是构造函数形参类型的值,则编译器将自动调用转换构造函数,这会在以下 4 种不同的上下文环境中出现:

  • 该类的一个对象使用转换构造函数的形参类型的值进行初始化。例如:

    IntClass intObject = 23;
  • 该类的一个对象被赋给了一个转换构造函数形参类型的值。例如:

    intObject = 24;
  • 函数需要的是该类的类型的值形参,但传递的却是构造函数的形参类型的值。例如,假设定义了下面这样一个函数:

    void printValue(IntClass x)
    {
        cout << x.getValue();
    }

    但是在调用它的时候却传递了一个整数给它:

    printValue(25);

    编译器将使用转换构造函数将整数 25 转换为 IntClass 类的对象,然后将该对象传递给该函数。如果形参是一个指针或对 IntClass 对象的引用,则编译器将不会调用转换构造函数。只有在形参按值传递时,才会调用转换构造函数。

  • 返回值为类类型的函数实际上返回的却是转换构造函数的形参类型的值。例如,编译器将接收以下函数:

    IntClass f(int intValue)
    {
        return intValue;
    }

    请注意,虽然已经将 IntClass 声明为其返回类型,但是该函数仍然会返回整数类型的值,于是编译器也将再次隐式地调用转换构造函数,将整数 intValue 转换为一个 IntClass 对象,这个对象正是需要从函数返回的对象。

3.拷贝构造函数

拷贝构造函数,顾名思义,等于拷贝 + 构造。它肩负着创建新对象的任务,同时还要负责把另外一个对象拷贝过来。

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:

  1. 一个对象以值传递的方式传入函数体,如果是引用传递则不会调用;
  2. 一个对象以值传递的方式从函数返回,或返回对象的引用;(经过测试,返回对象的引用不会调用拷贝构造函数
  3. 一个对象需要通过另外一个对象进行初始化。

针对1、2进行扩展说明

在C++中对象如何作为参数传递和返回? 来源地址:https://www.php.cn/csharp-article-416163.html

1、将对象作为参数传递

要将对象作为参数传递,我们将对象名作为参数写入,同时调用函数,方法与对其他变量执行是相同的。

基本语法:

函数名(对象名);
#include <iostream>
using namespace std;

class Example {
public:
    int a;
    // 此函数将对象作为参数
    void add(Example E)
    {
        a = a + E.a;
    }
};
int main()
{
    // 创建对象
    Example E1, E2;
    // 两个对象的值都已初始化
    E1.a = 50;
    E2.a = 100;
    cout << "初始值 \n";
    cout << "对象1的值: " << E1.a
         << "\n对象2的值: " << E2.a
         << "\n\n";
    // 将对象作为参数传递给函数add()
    E2.add(E1);
    // 传递给函数add()后
    cout << "新值 \n";
    cout << "对象1的值: " << E1.a
         << "\n对象2的值:" << E2.a
         << "\n\n";
    return 0;
}

调用E2.add(E1)时,会产生以下几个重要步骤:

(1) E1对象传入形参时,会先会产生一个临时变量,就叫 C 吧(该临时的值即为形参的值); (2) 然后调用拷贝构造函数把E1的值给C(这里调用的是默认的拷贝构造函数)。 整个这两个步骤有点像这条语句:Example C(test); (3) 等E2.add(E1)执行完后, 析构掉 C 对象。

2、将对象作为参数返回

基本语法:

object = return object_name;
#include <iostream>
using namespace std;
class Example {
public:
    int a;
    // 此函数将以对象为参数并返回对象
    Example add(Example Ea, Example Eb)
    {
        Example Ec;// 这种编译貌似通过不了,提示使用了未初始化的局部变量,采用我后面写的另一个可以通过
        Ec.a = Ec.a + Ea.a + Eb.a;
        // 返回对象
        return Ec;
    }
};
int main()
{
    Example E1, E2, E3;
    // 两个对象的值都已初始化
    E1.a = 50;
    E2.a = 100;
    E3.a = 0;
    cout << "初始值 \n";
    cout << "对象1的值: " << E1.a
         << " \n对象2的值: " << E2.a
         << "\n对象3的值: " << E3.a
         << "\n\n";
    //将对象作为参数传递给函数add()。
    E3 = E3.add(E1, E2);
    // 将对象作为参数传递后更改的值
    cout << "新值 \n";
    cout << "对象1的值: " << E1.a
         << " \n对象2的值: " << E2.a
         << " \n对象3的值: " << E3.a
         << "\n";
    return 0;
}
class Example {
	public:
		int a;
	public:
		Example(int num) :a(num) {}
		// 此函数将以对象为参数并返回对象的引用
		Example& add(Example Ea, Example Eb)
		{
			Example Ec(0);
			Ec.a = Ec.a + Ea.a + Eb.a;
			// 返回对象
			return Ec;
		}
	};

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该拷贝构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。

自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

这里还得再补充一下,经过自己的测试

对象以值传递或引用的方式从函数返回的其它细节

class Example {
	public:
		int a;
	public:
		Example(int num) :a(num) {}
		Example(const Example& e) { 
			this->a = e.a;
			cout << "copy\n"; }
		~Example() { cout << "destory\n"; }
		// 此函数将以对象为参数并返回对象
		Example add(Example Ea, Example Eb)
		{
			Example Ec(0);
			//Example Ec;
			Ec.a = Ec.a + Ea.a + Eb.a;
			// 返回对象
			return Ec;
		}
		Example change()
		{
			Example Ec(0);
			//Example Ec;
			Ec.a = Ec.a * 2;
			// 返回对象
			return Ec;
		}
	};
int main()
	{
		Example E1(0), E2(0), E3(0);
		E1.a = 50;
		E2.a = 100;
		//E3.a = 0;
		cout << "初始值 \n";
		cout << "对象1的值: " << E1.a
			<< " \n对象2的值: " << E2.a
			<< "\n对象3的值: " << E3.a
			<< "\n\n";
		//E3 = E3.add(E1, E2);
		E3 = E3.change();
		// 将对象作为参数传递后更改的值
		cout << "新值 \n";
		cout << "对象1的值: " << E1.a
			<< " \n对象2的值: " << E2.a
			<< " \n对象3的值: " << E3.a
			<< "\n";
	return 0;
}
// 执行E3 = E3.change();时输出
初始值
对象1的值: 50
对象2的值: 100
对象3的值: 0

copy // 因为返回的是对象
destory
destory
// 返回是引用的时候这个地方只输出一个destory
新值
对象1的值: 50
对象2的值: 100
对象3的值: 0
destory
destory
destory

分析:

1)执行函数返回对象时,会产生以下几个重要步骤:

(1) 先会产生一个临时变量,就叫XXXX吧。
(2) 然后调用拷贝构造函数把Ec的值给XXXX。整个这两个步骤有点像:Example XXXX(Ec); // 所以这里产生一个拷贝输出copy
(3) 在函数执行到最后先析构Ec局部变量。// 第一次析构Ec
(4) 等E3.change()执行完后再析构掉XXXX对象。// 第二次析构xxxx

2)如果change()函数返回的是对象的引用,那么不会产生拷贝动作,赋值给E3后,E3被析构调用一次析构函数。

3)如果调用add()函数,则输出如下:

copy // 拷贝E1
copy // 拷贝E2
copy // 拷贝Ec
destory // 析构Ec
destory // 析构E2
destory // 析构E1
destory // 析构临时变量xxxx

// 同样,如果返回的是引用,也会少一次构造与析构,即没有产生临时变量

特别注意:返回类型是引用,但返回的是这个对象本身,不是它的副本,返回引用与传引用类似,都是直接操作该对象本身

二.C++类的静态数据为什么一定要初始化

我们知道C++类的静态成员变量是需要初始化的,但为什么要初始化呢。

其实这句话“静态成员变量是需要初始化的”是有一定问题的,应该说“静态成员变量需要定义”才是准确的,而不是初始化

两者的区别在于:初始化是赋一个初始值,而定义是分配内存

静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存

可以通过以下几个例子更形象的说明这个问题:

//test.cpp 
#include <cstdio> 
class A { 
    public: 
        static int a; //声明但未定义
 }; 
int main() { 
    printf("%d", A::a);
    return 0;
}

编译以上代码会出现“对‘A::a’未定义的引用”错误。这是因为静态成员变量a未定义,也就是还没有分配内存,显然是不可以访问的。

再看如下例子:

//test.cpp 
#include <cstdio> 
class A { 
    public: 
        static int a; //声明但未定义
 }; 
int A::a = 3; //定义了静态成员变量,同时初始化。也可以写"int A::a;",即不给初值,同样可以通过编译
int main() { 
    printf("%d", A::a);
    return 0;
}

注解:静态变量定义时不给初始值,系统会默认设为0,默认值由编译器决定。

这样就对了,因为给a分配了内存,所以可以访问静态成员变量a了。

因为类中的静态成员变量仅仅是声明,暂时不需分配内存,所以我们甚至可以这样写代码:

//a.cpp
class B; //这里我们使用前置声明,完全不知道B是什么样子
class A {
    public:
        static B bb;//声明了一个类型为B的静态成员,在这里编译器并未给bb分配内存。
                    //因为仅仅是声明bb,所以编译器并不需要知道B是什么样子以及要给其对应的对象分配多大的空间。
                    //所以使用前置声明"class B"就可以保证编译通过。
};

注意:这里只可以用来声明,而不能用前置声明的类来定义成员,写为B bb就错了。

使用命令"g++ -c -o a.o a.cpp"通过编译。

对于类来说,new一个类对象不仅会分配内存,同时会调用构造函数进行初始化,所以类对象的定义和初始化总是关联在一起

三.C++中,能不能delete空指针

完全可以 . . . .

可能有不少人对Delete删除空指针的用法不屑一顾 , 但在实际运用当中 ,

却有不少人会犯类似的错误 , 最典型的如下:

if(pMyClass) //这里, pMyClass是指向某个类的指针 . .
{
  delete pMyClass ;
} 

他们往往先判断一下指针是否为空 , 如果不为空 , 说明没有被删除 ,

于是清空这个指针 . . .

出发点和逻辑思维是好的 , 但是却毫无必要 . . .

因为实际上delete 本身会自动检查对象是否为空 .如果为空 , 就不做操作 . .

所以直接用delete pMyClass 就可以了 . . .

删除空指针当然也是同样道理 . .

注意:

delete NULL 是有问题的,编译没通过
// 提示:不能删除不是指针的对象,表达式必须是指向完整对象类型的指针
char *p = NULL;

delete p;

2.delete栈上的空间是不行的

char *p = "1234";

delete p;

  char *p = new char;
  delete p;
  delete p; // 不能删除两次,第一次delete p之后,p的地址并不是空,同一块内存释放两次是有问题的

3.最好的风格是:

 if(pMyClass) //这里, pMyClass是指向某个类的指针 . .
 {
   delete pMyClass ;
   pMyClass = NULL; // 这句不能少
 }

因为这段代码在一个函数中,避免函数被调用两次而引起问题

四.静态成员函数详解

普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。

编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给 this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。而静态成员函数可以通过类来直接调用,编译器不会为它增加形参 this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数。

普通成员变量占用对象的内存,静态成员函数没有 this 指针,不知道指向哪个对象,无法访问对象的成员变量,也就是说静态成员函数不能访问普通成员变量,只能访问静态成员变量

普通成员函数必须通过对象才能调用(类定义时没有给普通成员函数分配内存,只有类对象创建的时候才分配),而静态成员函数没有 this 指针,无法在函数体内部访问某个对象,所以不能调用普通成员函数,只能调用静态成员函数。

静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。

下面是一个完整的例子,该例通过静态成员函数来获得学生的总人数和总成绩:

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

class Student{
public:
    Student(const string & name, int age, float score);
    void show();
public:  //声明静态成员函数
    static int getTotal();
    static float getPoints();
private:
    static int m_total;  //总人数
    static float m_points;  //总成绩
private:
    char *m_name;
    int m_age;
    float m_score;
};

int Student::m_total = 0;
float Student::m_points = 0.0;

Student::Student(const string & name, int age, float score): m_name(name), m_age(age), m_score(score){
    m_total++;
    m_points += score;
}
void Student::show(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
//定义静态成员函数
int Student::getTotal(){
    return m_total;
}
float Student::getPoints(){
    return m_points;
}

int main(){
    (new Student("小明", 15, 90.6)) -> show();
    (new Student("李磊", 16, 80.5)) -> show();
    (new Student("张华", 16, 99.0)) -> show();
    (new Student("王康", 14, 60.8)) -> show();

    int total = Student::getTotal();
    float points = Student::getPoints();
    cout<<"当前共有"<<total<<"名学生,总成绩是"<<points<<",平均分是"<<points/total<<endl;

    return 0;
}

运行结果: 小明的年龄是15,成绩是90.6 李磊的年龄是16,成绩是80.5 张华的年龄是16,成绩是99 王康的年龄是14,成绩是60.8 当前共有4名学生,总成绩是330.9,平均分是82.725


总人数 m_total 和总成绩 m_points 由各个对象累加得到,必须声明为 static 才能共享;getTotal()、getPoints() 分别用来获取总人数和总成绩,为了访问 static 成员变量,我们将这两个函数也声明为 static。

C++中,静态成员函数的主要目的是访问静态成员。getTotal()、getPoints() 当然也可以声明为普通成员函数,但是它们都只对静态成员进行操作,加上 static 语义更加明确

和静态成员变量类似,静态成员函数在声明时要加 static,在定义时不能加 static。静态成员函数可以通过类来调用(一般都是这样做),也可以通过对象来调用,上例仅仅演示了如何通过类来调用。

五.如何在类外访问私有成员?

1.通过公有的方法

    class A{
    private :
    	int a;
    public:
    	int Geta()
    	{
    		return a;
    	}
    };

2.指针访问

class A
{
public:
	A() :a(0),b(0),c(0) {}
public:
	int a;
	int Getb() { return b; };
private:
	int b;
	int c;
};
int main()
{
	A a;
	A* pa = &a;
	cout << a.Get(); // 输出0
	int* pc = (int*)((int)&a + 4); // (int)&a把a的地址强制转换成十进制的整型,内存地址虽然是用十六进制表示的,反映到十进制也是一样,以字节为单位进行增减,所以整型为4个字节,那与它相邻的地址即为它的地址的数值加上4。
	*pc = 10;
	cout << a.Get(); // 输出10
	system("pause");
	return 0;
}

通过此例也可以发现类对象、结构体与数组有类似之处,它们的名字取地址的值都与其首元素地址相同,当然可能也不绝对,只是这个例子反映的是这样。

六.C++中空类占一字节原因详解

C++中空类占位问题

在C++中空类会占一个字节,这是为了让对象的实例能够相互区别。具体来说,空类同样可以被实例化,并且每个实例在内存中都有独一无二的地址,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有独一无二的内存地址。如果没有这一个字节的占位,那么空类就无所谓实例化了,因为实例化的过程就是在内存中分配一块地址

注意:当该空白类作为基类时,该类的大小就优化为0了,这就是所谓的空白基类最优化。

#include<iostream>
using namespace std;
class test
{
};
class derive :public test
{
	private:
		int a;
};
int main()
{
	test a, b;
	cout << "sizeof(test): " << sizeof(test) << endl; // 1,同样如果test里包含一个静态数据成员,其大小依然为1
    cout << "sizeof(derive): " << sizeof(derive) << endl; // 4,即派生类大小不会加那个1的大小
	cout << "addr of a: " << &a << endl;
	cout << "addr of b: " << &b << endl;
	system("pause");
	return 0;
}

注意:空白基类最优化无法被施加于多重继承上只适合单一继承。我测试了好像是这样,继承一个空白类一个包含一个整型的数据成员,自身一个整型数据,大小是12字节。

七.C++类内初始化

C++11的类内初始化允许非static成员的初始化,可以用{}或=号。,在之前类内初始化是不允许的。

class A{
    static const int a = 7; //C++98允许
    int b = 8; //C++11允许,而98不允许
}

构造函数的初始化列表与类内成员初始化没有谁好谁不好,谁来替代谁,两种方法可相互补充使用。类内初始化有一些好处:

1、当你有多个构造函数时,如果使用初始化列表,每个构造函数都要写一遍,烦人不说,同时产生重复代码,修改易漏。如果把这些成员都用类内初始化,初始化列表就不用再列出它们了。

2、类内初始化,成员之间的顺序是隐式的,会有些便利。如果使用初始化列表,它是有顺序之分的,顺序不对,编译器会警告。

3、对于简单的类或结构,没有构造函数的,可以直接用类内初始化在成员声明的同时直接初始化,方便

对于一些类类型的成员初始化要小心,如果成员之间有依赖关系,这时使用初始化列表显式的指明这些成员的构造(初始化)顺序是比较稳妥的

如果成员已经使用了类内初始化,但在构造函数的初始化列表又列出来,编译器以后者优先,类内初始化会被忽略。如果某些成员使用不同构造函数时,会有不同的默认值,这种情况就要用初始化列表。同时,其它成员依然可以使用类内初始化。

类内初始化绝对不是解决什么内置类型默认初始化时未定义问题。面向对象编程一个很重要的原则,程序员有责任要保证对象产生出来,它的每个成员都必须是初始化的,这是设计问题以及基本意识,无论是使用哪种方法初始化。

补充:

由于类成员初始化总在构造函数体执行之前

从效率上:

如果在类构造函数里赋值:成员有类对象的时候在成员初始化时会调用一次其默认的构造函数,在类构造函数里又会调用一次成员的构造函数再赋值

如果在类构造函数使用初始化列表:仅在初始化列表里调用一次成员的构造函数并赋值;

在内部类型如int或者long或者其它没有构造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。

在程序中定义变量并初始化的机制中,有两种形式,一个是我们传统的初始化的形式,即赋值运算符赋值,还有一种是括号赋值,如:

int a=10; 
char b='r';\\赋值运算符赋值 
int a(10);\ 
char b('r');\\括号赋值 

以上定义并初始化的形式是正确的,可以通过编译,但括号赋值只能在变量定义并初始化中,不能用在变量定义后再赋值,这是和赋值运算符赋值的不同之处。

冒号初始化与函数体初始化的区别在于:

冒号初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么 分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行

对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。

进一步补充:

类对象的构造顺序是这样的:

1.分配内存,调用构造函数时,隐式/显示的初始化各数据成员;

2.进入构造函数后在构造函数中执行一般赋值与计算。

使用初始化列表有两个原因:

原因1.必须这样做:

《C++ Primer》中提到在以下三种情况下需要使用初始化成员列表:

情况一、需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);

情况二、需要初始化const修饰的类成员或初始化引用成员数据;

情况三、子类初始化父类的私有成员,需要在(并且也只能在)参数初始化列表中显示调用父类的构造函数

情况一的说明:数据成员是对象,并且这个对象只有含参数的构造函数,没有无参数的构造函数

  如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错。

注意:初始化列表在构造函数执行前执行(对同一个变量在初始化列表和构造函数中分别初始化,首先执行初始化列表,后在函数体内值,后者会覆盖前者)。

原因2.效率要求这样做:

类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对成员变量的赋值操作,显然,赋值和初始化是不同的,这样就体现出了效率差异,如果不用成员初始化列表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次赋值操作符的调用,如果是类对象,这样做效率就得不到保障

注意:构造函数需要初始化的数据成员,不论是否显示的出现在构造函数的成员初始化列表中,都会在该处完成初始化

关于前面提到的显示与隐式进行补充:

初始化阶段可以是显式的或隐式的取决于是否存在成员初始化表隐式初始化阶段按照声明的顺序依次调用所有基类的缺省构造函数然后是所有成员类对象的缺省构造函数

如:
Account::Account()
{ 
        _name = ""; 
        _balance = 0.0; 
        _acct_nmbr = 0; 
}

则初始化阶段是隐式的。在构造函数体被执行之前先调用与 _ name 相关联的缺省string构造函数。这意味着把空串赋给_name的赋值操作是没有必要的。对于类对象,在初始化和赋值之间的区别是巨大的。成员类对象应该总是在成员初始化表中被初始化而不是在构造函数体内被赋值。 缺省Account构造函数更正确的实现如下:

Account::Account() : _name( string() )
{ 
       _balance = 0.0; 
       _acct_nmbr = 0; 
}

它之所以更正确,是因为我们已经去掉了在构造函数体内不必要的对_name 的赋值。但是对于缺省构造函数的显式调用也是不必要的。下面是更紧凑但却等价的实现:

Account::Account()
{ 
      _balance = 0.0; 
      _acct_nmbr = 0; 
}

剩下的问题是:对于两个被声明为内置类型的数据成员其初始化情况如何?例如用成员初始化表和在构造函数体内初始化_balance是否等价?回答是不。对于非类数据成员的初始化或赋值除了两个例外,两者在结果和性能上都是等价的。即更受欢迎的实现是用成员初始化表:

// 更受欢迎的初始化风格
Account:: Account(): _balanae( 0.0 ), _acct_nmbr( 0 ) { }

两个例外是:const 和引用数据成员也必须是在成员初始化列表中被初始化,否则就会产生编译时刻错误。静态数据成员的要求是在类外初始化,不强调用初始化列表。

另外还有一种在创建对象时的显示/隐式初始化的说法:

A abc(200);//显式初始化,直接du调用构造函数

A c = 0;//这是一种隐式初始化,先调用构造函数,再调用了拷贝构造函数。即它首先调用A(int i) : m_i(i){}创建一个临时对象,再用这个临时对象初始化c,所以存在拷贝的动作。不信你可以把A的拷贝构造函数声明为私有的,这句就过不了编译。

A b = a;//用一个对象隐式初始化另一对象,调用拷贝构造函数,作用域结束时析构

显示初始化的就是 你直接调用构造函数或拷贝构造函数,函数写什么样,你就怎么样调用; 而隐式的就是不是函数调用的方式,但是后面操作的依然是构造函数和拷贝构造函数,只是看起来不明显而已。

八.C++函数重载详解

在实际开发中,有时候我们需要实现几个功能类似的函数,只是有些细节不同。例如希望交换两个变量的值,这两个变量有多种类型,可以是 int、float、char、bool 等,我们需要通过参数把变量的地址传入函数内部。在C语言中,程序员往往需要分别设计出四个不同名的函数,其函数原型与下面类似:

void swap1(int *a, int *b);      //交换 int 变量的值
void swap2(float *a, float *b);  //交换 float 变量的值
void swap3(char *a, char *b);    //交换 char 变量的值
void swap4(bool *a, bool *b);    //交换 bool 变量的值

但在C++中,这完全没有必要。C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading)。

参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。

重载就是在一个作用范围内(同一个类、同一个命名空间等)有多个名称相同但参数不同的函数。重载的结果是让一个函数名拥有了多种用途,使得命名更加方便(在中大型项目中,给变量、函数、类起名字是一件让人苦恼的问题),调用更加灵活

总结函数的重载的规则:

  • 函数名称必须相同;
  • 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等),仅仅参数名称不同是不可以的;
  • 函数的返回类型可以相同也可以不相同;
  • 仅仅返回类型不同不足以成为函数的重载。

C++ 是如何做到函数重载的

C++代码在编译时会根据参数列表对函数进行重命名,例如void Swap(int a, int b)会被重命名为_Swap_int_intvoid Swap(float x, float y)会被重命名为_Swap_float_float。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。

不同的编译器有不同的重命名方式,这里仅仅举例说明,实际情况可能并非如此。

从这个角度讲,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。

九.C++ class和struct到底有什么区别

C++ 中保留了C语言的 struct 关键字,并且加以扩充。在C语言中,struct 只能包含成员变量,不能包含成员函数。而在C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数

C++中的 struct 和 class 基本是通用的,唯有几个细节不同:

  • 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的;
  • class 继承默认是 private 继承,而 struct 继承默认是 public 继承;
  • class 可以使用模板,而 struct 不能。

C++ 没有抛弃C语言中的 struct 关键字,其意义就在于给C语言程序开发人员有一个归属感,并且能让C++编译器兼容以前用C语言开发出来的项目。

在编写C++代码时,我强烈建议使用 class 来定义类,而使用 struct 来定义结构体,这样做语义更加明确(所以不要用struct来定义类)。

十.多重继承时使用虚继承是如何保证只有一个间接基类的副本的

在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的

下面我们以菱形继承为例来演示构造函数的调用:

#include <iostream>
using namespace std;

//虚基类A
class A{
public:
    A(int a);
protected:
    int m_a;
};
A::A(int a): m_a(a){ }

//直接派生类B
class B: virtual public A{
public:
    B(int a, int b);
public:
    void display();
protected:
    int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<endl;
}

//直接派生类C
class C: virtual public A{
public:
    C(int a, int c);
public:
    void display();
protected:
    int m_c;
};
C::C(int a, int c): A(a), m_c(c){ }
void C::display(){
    cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
}

//间接派生类D
class D: public B, public C{
public:
    D(int a, int b, int c, int d);
public:
    void display();
private:
    int m_d;
};
50:D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }
void D::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}

int main(){
    B b(10, 20);
    b.display();
   
    C c(30, 40);
    c.display();

    D d(50, 60, 70, 80);
    d.display();
    return 0;
}
运行结果:
m_a=10, m_b=20
m_a=30, m_c=40
m_a=50, m_b=60, m_c=70, m_d=80

请注意第 50 行代码,在最终派生类 D 的构造函数中,除了调用 B 和 C 的构造函数,还调用了 A 的构造函数,这说明 D 不但要负责初始化直接基类 B 和 C,还要负责初始化间接基类 A。而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类,用户尝试调用间接基类的构造函数将导致错误

现在采用了虚继承,虚基类 A 在最终派生类 D 中只保留了一份成员变量 m_a,如果由 B 和 C 初始化 m_a,那么 B 和 C 在调用 A 的构造函数时很有可能给出不同的实参,这个时候编译器就会犯迷糊,不知道使用哪个实参初始化 m_a。

为了避免出现这种矛盾的情况C++ 干脆规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。在第 50 行代码中,调用 B 的构造函数时试图将 m_a 初始化为 90,调用 C 的构造函数时试图将 m_a 初始化为 100,但是输出结果有力地证明了这些都是无效的,m_a 最终被初始化为 50,这正是在 D 中直接调用 A 的构造函数的结果。

另外需要关注的是构造函数的执行顺序。虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。

修改本例中第 50 行代码,改变构造函数出现的顺序:

D::D(int a, int b, int c, int d): B(90, b), C(100, c), A(a), m_d(d){ }

虽然我们将 A() 放在了最后,但是编译器仍然会先调用 A(),然后再调用 B()、C(),因为 A() 是虚基类的构造函数,比其他构造函数优先级高。如果没有使用虚继承的话,那么编译器将按照出现的顺序依次调用 B()、C()、A()。

十一.C++构造函数初始化列表

使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。

初始化列表可以用于全部成员变量,也可以只用于部分成员变量。下面的示例只对 m_name 使用初始化列表,其他成员变量还是一一赋值:

Student::Student(char *name, int age, float score): m_name(name)
{m_age = age;    
 m_score = score;}

注意,成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。请看代码:

#include <iostream>
using namespace std;

class Demo{
private:
    int m_a;
    int m_b;
public:
    Demo(int b);
    void show();
};

Demo::Demo(int b): m_b(b), m_a(m_b){ }
void Demo::show(){ cout<<m_a<<", "<<m_b<<endl; }

int main(){
    Demo obj(100);
    obj.show();
    return 0;
}
// 运行结果
-858993460, 100

在初始化列表中,我们将 m_b 放在了 m_a 的前面,看起来是先给 m_b 赋值,再给 m_a 赋值,其实不然!成员变量的赋值顺序由它们在类中的声明顺序决定,在 Demo 类中,我们先声明的 m_a,再声明的 m_b,所以构造函数和下面的代码等价:

Demo::Demo(int b): m_b(b), m_a(m_b)
{m_a = m_b;    
 m_b = b;}

给 m_a 赋值时,m_b 还未被初始化,它的值是不确定的,所以输出的 m_a 的值是一个奇怪的数字;给 m_a 赋值完成后才给 m_b 赋值,此时 m_b 的值才是 100。

obj 在栈上分配内存,成员变量的初始值是不确定的。

再说一下构造顺序的事:

#include <iostream>
using namespace std;
class D {
public:
	D() { cout << "construct D\n"; }
	~D() { cout << "destroy D\n"; }
};
class Demo {
private:
	
	int m_b;
	int m_a;
	D m_d;
public:
	//Demo(int a, int b );
	Demo(int b, D& d);
	void show();
	~Demo() { cout << "destory Demo\n"; }
};

Demo::Demo(int b,D &d) : m_b(b), m_a(m_b), m_d(d){  cout << "construct Demo\n"; }
void Demo::show() { cout << m_a << ", " << m_b << endl; }

int main() {
	D d;
	Demo obj(100,d);
	//obj.show();
	return 0;
}
// 输出如下:
construct D
construct Demo
destory Demo
destroy D
destroy D

可以看到:类中有成员为另一个类对象时先构造这个对象调用它的构造函数,再调用自身的构造函数;析构顺序相反。

十二.定义一个对象和new一个对象的区别

类到对象的过程就是实例化的过程,我经常会看到两种方式,一种是这样的:

class A{
。。。
}
void main(){
A a;
。。。
}

另外一种是这样的:

class A{
。。。
}
void main(){
A *a=new A();
}

上面两种方式一种可以看作是在主函数定义一个对象,另一个可以看作是new一个对象,主要有以下几点不同:

1、他们的存储空间不同,直接定义一个对象放在栈上,new一个对象放在堆上。换句话说,new出来的对象的生命周期是全局的,譬如在一个函数块里new一个对象,可以将该对象的指针返回回去,该对象依旧存在。而声明的对象的生命周期只存在于声明了该对象的函数块中如果返回该声明的对象,将会返回一个已经被销毁的对象

2、使用场合不同,由于栈较小并且主要用于存储临时变量,所以定义一个对象在{}的作用域生命周期就完了,new的对象放在堆上,可以通过函数返回他的指针,并且需要手工去销毁(delete)这个对象否则会出现内存泄漏;而直接使用类名创建的对象,则不需要手动销毁该对象,因为该类的析构函数会自动执行;

3、new的特性:

  • new创建类对象需要指针接收,一处初始化,多处使用
  • new创建类对象使用完需delete销毁
  • new创建对象直接使用堆空间,而局部不用new定义类对象则使用栈空间
  • new对象指针用途广泛,比如作为函数返回值、函数参数等
  • 频繁调用(不同)对象的场合并不适合new方法创建对象,原理同new申请和delete释放内存相同

注意:类对象通过new创建,通过delete调用析构函数,所以显然new一个对象的时候调用了构造函数,测试过确实如此。

class A{
	public:
		void foo() { printf("foo"); }
		virtual void bar() { printf("bar"); }
		A() { bar(); }
	};
	class B :public A {
	public:
		B() { cout << "create"; }
		void foo() { printf("b_foo"); }
		void bar() { printf("b_bar"); }
	};
	int main() {
		//B *b = new B();
		//A *p = b; // 这句不会调用构造函数,也不会调用拷贝构造函数,只是指针的赋值,而用对象直接赋值的时候才调用拷贝构造函数,比如:A p = *b;而且调用的是A的拷贝构造函数,把*b对象作为实参传给A的拷贝构造函数去生成新对象p
        
		A* p = new B();// 与上面两句等价,先动态创建一个B类,再将返回的指针赋值给指向基类的指针,创建B类时先调用基类的构造函数再调用B类自己的构造函数
        输出:barcreate
		//p->foo();
		//p->bar();
	}

十三.C++中const在函数名前面和函数后面的区别

1.首先要知道函数名后面加const只能用于类的非静态成员函数。此类型的函数可称为只读成员函数,即成员变量为read only(例外情况见2)。

int const func();//合法,相当于const int func();
int func2() const;//非法,对函数的const限定词只能用于成员函数

在成员函数中,const加在函数名前和后也是有区别的。 例如:

class a{
int const func();
int func() const;
};
int const a::func() {return 0;}
int a::func() const {return 0;}

上面的代码是合法的,其中a::func成员函数是一个重载成员函数,两个函数都返回int类型数据(注意:对于c/c++,返回类型或参数类型中,*const int和int被认为是一种类型,浮点数返回为这两种结果是一样。但是const int 和 int * 不是一种类型 ),这两个重载函数正是基于函数名后的const来重载的。

int const func();表示该成员函数的隐藏this指针参数是a * const类型的;

而int func() const;表示该重载成员函数的隐藏this指针参数是a const * const类型的。

总结起来就是:

在普通的非const成员函数中,this的类型是一个指向类类型的 const指针。可以改变this所指向的值,但不能改变 this所保存的地址。 在 const成员函数中,this的类型是一个指向 const类类型对象的 const指针。既不能改变 this所指向的对象,也不能改变 this所保存的地址。

a *const 类型和 a const *const 类型是不同类型,因此可以重载。

由此可见const放在函数名后和名前是不同的。

const 加在函数前面是修饰函数的返回值,一般来说没多大意义,除非函数的返回值是引用。const只能加在类的成员后面做修饰,一般函数是不允许加在后面的,表示这个函数不会改变类成员属性的值。

说明:类中const(函数后面加)与static不能同时修饰成员函数,原因有以下两点

①C++编译器在实现const的成员函数时,为了确保该函数不能修改类的实例状态,会在函数中添加一个隐式的参数const this*。但当一个成员函数为static的时候,该函数是没有this指针的,也就是说此时const的用法和static是冲突的;

②两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系,因此不能同时用它们。

2.const与mutable的区别

从字面意思知道,mutalbe是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量(成员变量)将永远处于可变的状态,即使在一个const函数中。因此,后const成员函数中可以改变类的mutable类型的成员变量。

#include <iostream>
using namespace std;

class A{
private:
	int m_a;//int前加mutable关键字修饰即可编译通过
public:
	A():m_a(0){}
	int setA(int a) const
	{
		this->m_a = a;//error: l-value specifies const object
	}
	int setA(int a)
	{
		this->m_a = a;
	}
};

int main()
{
	A a1;
	return 0;
}

经过测试确实如此!

十四.函数签名

C++中的函数签名(function signature):包含了一个函数的信息,包括函数名、参数类型、参数个数、顺序以及它所在的类和命名空间。

普通函数签名并不包含函数返回值部分,如果两个函数仅仅只有函数返回值不同,那么系统是无法区分这两个函数的,此时编译器会提示语法错误。

函数签名用于识别不同的函数,函数的名字只是函数签名的一部分。在编译器及链接器处理符号时,使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称(decorated name)。编译器在将C++源代码编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名,也就是说,C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。C++编译器和链接器都使用符号来识别和处理函数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数

不同的编译器厂商的名称修饰方法可能不同,所以不同的编译器对于同一个函数签名可能对应不同的修饰后名称。

For functions that are specializations of function templates, the signature includes the return type. For functions that are not specializations, the return type is not part of the signature.

对于函数模板专门化的函数,签名包括返回类型。对于非专门化的函数,返回类型不是签名的一部分。

A function signature consists of the function prototype. What it tells you is the general information about a function, its name, parameters, what scope it is in, and other miscellaneous information.

函数签名由函数原型组成。它告诉你的是关于一个函数的一般信息,它的名字,参数,它的作用域,以及其他杂项信息。

Two overloaded functions must not have the same signature.

两个重载函数不能有相同的签名。

Default Arguments: The last parameter or parameters in a function signature may be assigned a default argument, which means that the caller may leave out the argument when calling the function unless they want to specify some other value.

默认参数:函数签名中的最后一个或多个参数可能会被赋予一个默认参数,这意味着调用者在调用函数时可以忽略这个参数,除非他们想指定一些其他的值。

十五.size_t和size_type

为了使自己的程序有很好的移植性,C++程序员应该尽量使用size_t和size_type,而不是int,unsigned。

在标准C/C++的语法中,只有int float char bool等基本的数据类型,至于size_t,或size_type都是以后的编程人员为了方便记忆所定义的一些便于理解的由基本数据类型的变体类型。

size_t是为了方便系统之间的移植而定义的,它是一个无符号整型,在32位系统上定义为:unsigned int;在64位系统上定义为unsigned long。size_t一般用来计数,sizeof操作符的结果类型是size_t,该类型保证能容纳实现所建立的最大对象的字节大小。它的意义大致是“适用于内存中可容纳的数据项目的个数的无符号整数类型”所以,它在数组下标和内存管理函数之类的地方广泛使用。例如:typedef unsigned int size_t;定义了size_t为整型。因为size_t类型的数据其实是保存了一个整数,所以它也可以做加减乘除,也可以转化为int并赋值给int类型的变量。类似的还有wchar_t, ptrdiff_t等。

size_type是由string类类型和vector类类型定义的类型,用于保存任意string对象或vector对象的长度,标准库类型将size_type定义为unsigned类型。string::size_type它在不同的机器上,长度可以是不同的,并非固定的长度,但只要你使用了这个类型,就是的你的程序适这个机器,与实际机器匹配。

size_t和size_type的主要区别:

  1. size_t是全局定义的类型;size_type是STL类中定义的类型属性。在使用STL中表明容器长度的时候,我们一般用size_type。

  2. string::size_type 类型一般就是unsigned int, 但是不同机器环境长度可能不同 win32 和win64上长度差别; size_t一般也是unsigned int

  3. size_t 使用的时候头文件需要 (貌似vs2019中不需要包含);size_type 使用的时候需要或者

  4. 下述长度均相等,长度为 win32:4 win64:8

    sizeof(string::size_type)

    sizeof(vector::size_type)

    sizeof(vector::size_type)

    sizeof(size_t)

  5. 二者联系:在用下标访问元素时,vector使用vector::size_type作为下标类型(size_type是容器概念,没有容器不能使用),而数组下标的正确类型则是size_t