C++对象初始化的顺序
构造函数调用顺序:
按照派生类继承基类的顺序,即 派生列表中声明的顺序
,依次调用基类的构造函数;
按照派生类中 成员变量
的声名顺序,依次调用派生类中成员变量所属类的构造函数;
执行派生类自身的构造函数。
综上可以得出,类对象的初始化顺序:基类构造函数–>派生类成员变量的构造函数–>自身构造函数
注:
基类构造函数的调用顺序与派生类的 派生列表
中的顺序有关;成员变量
的初始化顺序与 声明顺序
有关;
析构顺序和构造顺序相反。
#include <iostream>
using namespace std;
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()
*/
禁止一个类被实例化
方法一
using namespace std;
class A { public: int var1, var2; A(){ var1 = 10; var2 = 20; } virtual void fun() = 0; // 纯虚函数 };
int main() { A ex1; // error: cannot declare variable ‘ex1’ to be of abstract type ‘A’ return 0; }
方法二<br />将类的构造函数声明为私有private
<a name="KApmg"></a>
#### 为什么用成员初始化列表会快一些
**说明**:数据类型可分为内置类型和用户自定义类型(类类型),对于用户自定义类型,利用成员初始化列表效率高。<br />原因:用户自定义类型如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;如果在构造函数中初始化,**因为 C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,那么在执行构造函数的函数体之前首先调用默认的构造函数为成员变量设初值,在进入函数体之后,调用该成员变量对应的构造函数**。因此,使用列表初始化会 `减少调用默认的构造函数的过程`,效率高。
```cpp
#include <iostream>
using namespace std;
class A
{
private:
int val;
public:
A()
{
cout << "A()" << endl;
}
A(int tmp)
{
val = tmp;
cout << "A(int " << val << ")" << endl;
}
};
class Test1
{
private:
A ex;
public:
Test1() : ex(1) // 成员列表初始化方式
{
}
};
class Test2
{
private:
A ex;
public:
Test2() // 函数体中赋值的方式
{
ex = A(2);
}
};
int main()
{
Test1 ex1;
cout << endl;
Test2 ex2;
return 0;
}
/*
运行结果:
A(int 1)
A()
A(int 2)
*/
从程序运行结果可以看出,使用成员列表初始化的方式会省去调用默认的构造函数的过程。
实例化一个对象需要哪几个阶段
分配空间
创建类对象首先要为该对象分配内存空间。不同的对象,为其分配空间的时机未必相同。全局对象、静态对象、分配在栈区域内的对象,在编译阶段进行内存分配;存储在堆空间的对象,是在运行阶段进行内存分配。初始化
首先明确一点:初始化不同于赋值。初始化发生在赋值之前,初始化随对象的创建而进行,而赋值是在对象创建好后,为其赋上相应的值。这一点可以联想下上一个问题中提到:初始化列表先于构造函数体内的代码执行,初始化列表执行的是数据成员的初始化过程,这个可以从成员对象的构造函数被调用看的出来。赋值
对象初始化完成后,可以对其进行赋值。对于一个类的对象,其成员变量的赋值过程发生在类的构造函数的函数体中。当执行完该函数体,也就意味着类对象的实例化过程完成了。(总结:构造函数实现了对象的初始化和赋值两个过程,对象的初始化是通过初始化列表来完成,而对象的赋值则才是通过构造函数的函数体来实现。)
注:对于拥有虚函数的类的对象,还需要给虚表指针赋值。
没有继承关系的类,分配完内存后,首先给虚表指针赋值,然后再列表初始化以及执行构造函数的函数体,即上述中的初始化和赋值操作。
有继承关系的类,分配内存之后,首先进行基类的构造过程,然后给该派生类的虚表指针赋值,最后再列表初始化以及执行构造函数的函数体,即上述中的初始化和赋值操作。
静态绑定和动态绑定
静态类型和动态类型:静态类型
:变量在声明时的类型,是在编译阶段确定的。静态类型不能更改。动态类型
:目前所指对象的类型,是在运行阶段确定的。动态类型可以更改。
静态绑定和动态绑定:静态绑定
: 是指程序在 编译阶段 确定对象的类型(静态类型)。动态绑定
: 是指程序在 运行阶段 确定对象的类型(动态类型)。
静态绑定和动态绑定的区别:
发生的时期不同:如上。
对象的静态类型不能更改,动态类型可以更改。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun() { cout << "Base::fun()" << endl;
}
};
class Derive : public Base
{
public:
void fun() { cout << "Derive::fun()";
}
};
int main()
{
Base *p = new Derive(); // p 的静态类型是 Base*,动态类型是 Derive*
p->fun(); // fun 是虚函数,运行阶段进行动态绑定
return 0;
}
/*
运行结果:
Derive::fun()
编译时多态和运行时多态的区别
编译时多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。
运行时多态:在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。
编译时多态和运行时多态的区别:
时期不同:编译时多态发生在程序编译过程中,运行时多态发生在程序的运行过程中;
实现方式不同:编译时多态运用泛型编程来实现,运行时多态借助虚函数来实现。
深拷贝和浅拷贝
如果一个类拥有资源,该类的对象进行复制时,如果资源重新分配,就是深拷贝,否则就是浅拷贝。
深拷贝:该对象和原对象占用不同的内存空间,既拷贝存储在 栈空间
中的内容,又拷贝存储在 堆空间
中的内容。
浅拷贝:该对象和原对象占用同一块内存空间,仅拷贝类中位于 栈空间
中的内容。
当类的成员变量中有 指针变量
时,最好使用深拷贝。因为当两个对象指向同一块内存空间,如果使用浅拷贝,当其中一个对象的删除后,该块内存空间就会被释放,另外一个对象指向的就是垃圾内存。
浅拷贝示例
class Test
{
private:
int *p;
public:
Test(int tmp)
{
this->p = new int(tmp);
cout << "Test(int tmp)" << endl;
}
~Test()
{
if (p != NULL)
{
delete p;
}
cout << "~Test()" << endl;
}
};
int main()
{
Test ex1(10);
Test ex2 = ex1;
return 0;
}
/*
运行结果:
Test(int tmp)
~Test()
mycopy(7608,0x115377e00) malloc: *** error for object 0x7fef64405a10: pointer being freed was not allocated
mycopy(7608,0x115377e00) malloc: *** set a breakpoint in malloc_error_break to debug
*/
上述代码中,类对象 ex1、ex2 实际上是指向同一块内存空间,对象析构时,ex2 先将内存释放了一次,之后 析构对象 ex1 时又将这块已经被释放过的内存再释放一次。对同一块内存空间释放了两次,会导致程序崩溃。
深拷贝示例
#include <iostream>
using namespace std;
class Test
{
private:
int *p;
public:
Test(int tmp)
{
p = new int(tmp);
cout << "Test(int tmp)" << endl;
}
~Test()
{
if (p != NULL)
{
delete p;
}
cout << "~Test()" << endl;
}
Test(const Test &tmp) // 定义拷贝构造函数
{
p = new int(*tmp.p);
cout << "Test(const Test &tmp)" << endl;
}
};
int main()
{
Test ex1(10);
Test ex2 = ex1;
return 0;
}
/*
Test(int tmp)
Test(const Test &tmp)
~Test()
~Test()
*/
如何让类不能被继承
利用final关键字
用final关键字修饰的类不能被继承
#include <iostream>
using namespace std;
class Base final
{
};
class Derive: public Base{ // error: cannot derive from 'final' base 'Base' in derived type 'Derive'
};
int main()
{
Derive ex;
return 0;
}
借助友元,虚继承和私有构造函数
#include <iostream>
using namespace std;
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
};
int main(){
B b;
return 0;
}
说明:在上述代码中 B 类是不能被继承的类。
具体原因:
虽然 Base 类构造函数和析构函数被声明为私有 private,在 B 类中,由于 B 是 Base 的友元,因此可以访问 Base 类构造函数,从而正常创建 B 类的对象;
B 类继承 Base 类采用虚继承的方式,创建 C 类的对象时,C 类的构造函数要负责 Base 类的构造,但是 Base 类的构造函数私有化了,C 类没有权限访问。因此,无法创建 C 类的对象, B 类是不能被继承的类。
注意:在继承体系中,友元关系不能被继承,虽然 C 类继承了 B 类,B 类是 Base 类的友元,但是 C 类和 Base 类没有友元关系。
std::move 函数的实现原理
std::move函数的原型
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
说明:引用折叠原理
右值传递给上述函数的形参 T&& 依然是右值,即 T&& && 相当于 T&&。
左值传递给上述函数的形参 T&& 依然是左值,即 T&& & 相当于 T&。
小结:通过引用折叠原理可以知道,move() 函数的形参既可以是左值也可以是右值。
remove_reference 具体实现:
//原始的,最通用的版本
template <typename T> struct remove_reference{
typedef T type; //定义 T 的类型别名为 type
};
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a; //使用原版本,
remove_refrence<decltype(i)>::type b; //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type b; //右值引用特例版本
举例
int var = 10;
转化过程:
1. std::move(var) => std::move(int&& &) => 折叠后 std::move(int&)
2. 此时:T 的类型为 int&,typename remove_reference<T>::type 为 int,这里使用 remove_reference 的左值引用的特例化版本
3. 通过 static_cast 将 int& 强制转换为 int&&
整个std::move被实例化如下
string&& move(int& t)
{
return static_cast<int&&>(t);
}
总结:
std::move() 实现原理:
利用引用折叠原理将右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&& 变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变;
然后通过 remove_refrence 移除引用,得到具体的类型 T;
最后通过 static_cast<> 进行强制类型转换,返回 T&& 右值引用。
左值和右值的区别?左值引用和右值引用的区别?如果将左值转换成为右值?
左值
:指表达式结束后依然存在的持久对象。右值
:表达式结束就不再存在的临时对象。
左值和右值的区别:左值持久,右值短暂
右值引用和左值引用的区别:
左值引用不能绑定到要转换的表达式、字面常量或返回右值的表达式。右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。
右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。
std::move 可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义。
#include <iostream>
using namespace std;
void fun1(int& tmp)
{
cout << "fun1(int& tmp):" << tmp << endl;
}
void fun2(int&& tmp)
{
cout << "fun2(int&& tmp)" << tmp << endl;
}
int main()
{
int var = 11;
fun1(12); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
fun1(var);
fun2(1);
}
泛型编程
泛型编程实现的基础:模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。
泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。面试者可选择自己掌握比较扎实的一方面进行展开。
容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。
迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。
模板:可参考本章节中的模板相关问题。
类型萃取
类型萃取使用模板技术来萃取类型(包含自定义类型和内置类型)的某些特性,用以判断该类型是否含有某些特性,从而在泛型算法中来对该类型进行特殊的处理用来提高效率或者其他。
C++ 类型萃取一般用于模板中,当我们定义一个模板函数后,需要知道模板类型形参并加以运用时就可以用类型萃取。
比如我们需要在函数中进行拷贝,通常我们可以用内置函数 memcpy 或者自己写一个 for 循环来进行拷贝。
参考资料
https://blog.csdn.net/doo66/article/details/52208922
https://www.w3cschool.cn/cpp/cpp-a9no2ppi.html