类
C++ 编译器会给一个空类自动生成哪些函数?
**空类的大小:**空类声明时编译器不会生成任何成员函数:对于空类,声明编译器不会生成任何的成员函数,只会生成 1 个字节的占位符。当空类 A 定义对象时,sizeof(A) 仍是为 1。
空类定义时编译器会生成 6 个成员函数:
编译器会生成 6 个成员函数:缺省的构造函数、拷贝构造函数、析构函数、赋值运算符、两个取址运算符。
class A
{}; 该空类的等价写法如下:
class A
{
public:
A(){}; // 缺省构造函数
A(const A &tmp){}; // 拷贝构造函数
~A(){}; // 析构函数
A &operator=(const A &tmp){}; // 赋值运算符
A *operator&() { return this; }; // 取址运算符
const A *operator&() const { return this; }; // 取址运算符(const版本)
};
C++ 类对象的初始化顺序
构造函数调用顺序:
- 按照派生类继承基类的顺序,即派生列表中声明的顺序,依次调用基类的构造函数;
- 按照派生类中成员变量的声名顺序,依次调用派生类中成员变量所属类的构造函数;
- 执行派生类自身的构造函数。
综上可以得出,类对象的初始化顺序:基类构造函数–>派生类成员变量的构造函数–>自身构造函数。
基类构造函数的调用顺序与派生类的派生列表中的顺序有关;成员变量的初始化顺序与声明顺序有关;析构顺序和构造顺序相反。
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
};
class B
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
};
class Test : public A, public B // 派生列表
{
public:
Test() { cout << "Test()" << endl; }
~Test() { cout << "~Test()" << endl; }
private:
B ex1;
A ex2;
};
int main()
{
Test ex;
return 0;
}
//运行结果:A() -> B() -> B() -> A() -> Test() -> ~Test() -> ~A() -> ~B() -> ~B() -> ~A()
首先调用基类 A 和 B 的构造函数,按照派生列表 public A, public B 的顺序构造;
然后调用派生类 Test 的成员变量 ex1 和 ex2 的构造函数,按照派生类中成员变量声明的顺序构造;
最后调用派生类的构造函数;
接下来调用析构函数,和构造函数调用的顺序相反。
实现一个类成员函数,要求不允许修改类的成员变量?
如果想达到一个类的成员函数不能修改类的成员变量,只需用 const 关键字来修饰该函数即可。即不能在 const 修饰的成员函数中修改成员变量的值,除非该成员变量用 mutable 修饰。mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
使用mutable的注意事项:
- mutable只能作用于类的非静态和非常量数据成员。
- 在一个类中,应尽量或者不用mutable,大量使用mutable表示程序设计存在缺陷。
如何禁止一个类被实例化?
方法一:在类中定义一个纯虚函数,使该类成为抽象基类,因为不能创建抽象基类的实例化对象;
方法二:将类的构造函数声明为私有 private。
方法三:将所有构造函数 =delete。
如何让类不能被继承?
解决方法一:借助 final 关键字,用该关键字修饰的类不能被继承。
解决方法二:借助友元、虚继承和私有构造函数来实现。
template <typename T>
class Base{
friend T;
private:
Base(){
cout << "base" << endl;
}
~Base(){}
};
class B:virtual public Base<B>{ //一定注意 必须是虚继承
public:
B(){
cout << "B" << endl;
}
};
class C:public B{
public:
C(){} // error: 'Base<T>::Base() [with T = B]' is private within this context
};
实例化一个对象需要哪几个阶段?
- 分配空间:创建类对象首先要为该对象分配内存空间。不同的对象,为其分配空间的时机未必相同。全局对象、静态对象、分配在栈区域内的对象,在编译阶段进行内存分配;存储在堆空间的对象,是在运行阶段进行内存分配。
- 初始化:首先明确一点:初始化不同于赋值。初始化发生在赋值之前,初始化随对象的创建而进行,而赋值是在对象创建好后,为其赋上相应的值。这一点可以联想下上一个问题中提到:初始化列表先于构造函数体内的代码执行,初始化列表执行的是数据成员的初始化过程,这个可以从成员对象的构造函数被调用看的出来。
- 赋值:对象初始化完成后,可以对其进行赋值。对于一个类的对象,其成员变量的赋值过程发生在类的构造函数的函数体中。当执行完该函数体,也就意味着类对象的实例化过程完成了。(总结:构造函数实现了对象的初始化和赋值两个过程,对象的初始化是通过初始化列表来完成,而对象的赋值是通过构造函数的函数体来实现。)
对于拥有虚函数的类的对象,还需要给虚表指针赋值。
- 没有继承关系的类,分配完内存后,首先给虚表指针赋值,然后再列表初始化以及执行构造函数的函数体,即上述中的初始化和赋值操作。
- 有继承关系的类,分配内存之后,首先进行基类的构造过程,然后给该派生类的虚表指针赋值,最后再列表初始化以及执行构造函数的函数体,即上述中的初始化和赋值操作。
构造函数
默认构造函数
未提供任何实参,来控制默认初始化过程的构造函数称为默认构造函数。
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类之前创建的对象来初始化新创建的对象。函数的名称必须和类名称一致,且函数的形参是本类的对象的引用。
拷贝构造函数通常用于(调用时机):
- 通过使用另一个同类型的对象来初始化新创建的对象(赋值初始化)。
- 复制对象把它作为参数传递给函数(值传递的方式)。
- 复制对象,并从函数返回这个对象(值传递的方式)。
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,或有动态内存分配,则它必须有一个拷贝构造函数。在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。拷贝构造函数的最常见形式如下:
classname (const classname &obj) {
// 构造函数的主体
}
浅拷贝:
指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题。执行浅拷贝,那么两个指针指向了堆里的同一个空间,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。
深拷贝:
在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间。执行深拷贝,那么两个指针指向了堆里的不同空间,但它们指向的空间具有相同的内容。
通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。
拷贝构造函数里能调用private成员变量。类中可以存在超过一个拷贝构造函数。
浅拷贝和深拷贝的区别:
拷贝只是对浅指针的拷贝,拷贝后两个指针指向同一个内存空间。深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
为什么拷贝构造函数必须为引用?
原因:避免拷贝构造函数无限制的递归,最终导致栈溢出。
如果拷贝构造函数中形参不是引用类型,A ex3 = ex1;会出现什么问题?
构造 ex3,实质上是 ex3.A(ex1);,假如拷贝构造函数参数不是引用类型,那么将使得 ex3.A(ex1); 相当于 ex1 作为函数 A(const A tmp)的形参,在参数传递时相当于 A tmp = ex1,因为 tmp 没有被初始化,所以在 A tmp = ex1 中继续调用拷贝构造函数,接下来的是构造 tmp,也就是 tmp.A(ex1) ,必然又会有 ex1 作为函数 A(const A tmp); 的形参,在参数传递时相当于即 A tmp = ex1,那么又会触发拷贝构造函数,就这下永远的递归下去。
class A
{
private:
int val;
public:
A(int tmp) : val(tmp) // 带参数构造函数
{
cout << "A(int tmp)" << endl;
}
A(const A &tmp) // 拷贝构造函数
{
cout << "A(const A &tmp)" << endl;
val = tmp.val;
}
A &operator=(const A &tmp) // 赋值函数(赋值运算符重载)
{
cout << "A &operator=(const A &tmp)" << endl;
val = tmp.val;
return *this;
}
void fun(A tmp)
{
}
};
如何减少构造函数开销?
在构造函数中使用类初始化列表,会减少调用默认的构造函数产生的开销。
Test1() : ex(1) // 成员列表初始化方式
为什么用成员初始化列表会快一些?
数据类型可分为内置类型和用户自定义类型(类类型),对于用户自定义类型,利用成员初始化列表效率高。
原因: 用户自定义类型如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;如果在构造函数中初始化,因为 C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,那么在执行构造函数的函数体之前首先调用默认的构造函数为成员变量设初值,在进入函数体之后,调用该成员变量对应的构造函数。因此,使用列表初始化会减少调用默认的构造函数的过程,效率高。
如何避免拷贝?
最直观的想法是:将类的拷贝构造函数和赋值构造函数声明为私有 private,但对于类的成员函数和友元函数依然可以调用,达不到完全禁止类的对象被拷贝的目的,而且程序会出现错误,因为未对函数进行定义。
方法一:为类的构造函数增加 = delete 修饰符
方法二:”定义一个基类,将其中的拷贝构造函数和赋值构造函数声明为私有private;派生类以私有 private 的方式继承基类。
能够保证,在派生类 A 的成员函数和友元函数中无法进行拷贝操作,因为无法调用基类 Uncopyable 的拷贝构造函数或赋值构造函数。同样,在类的外部也无法进行拷贝操作。
如何判断是不是拷贝构造函数?
对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数。
析构函数
作用: 对象消亡时,自动被调用,用来释放对象占用的空间,避免内存泄漏。
特点: 名字与类名相同;在前面需要加上"~";无参数,无返回值;一个类最多只有一个析构函数;不显示定义析构函数会调用缺省析构函数。