文章目录
- 什么是虚函数?什么是纯虚函数?
- 虚函数和纯虚函数的区别
- 哪些函数不能声明为虚函数?
- 虚函数的实现机制
- 如何禁止构造函数的使用
- 什么是类的默认构造函数?
- 构造函数,析构函数是否需要定义成虚函数?为什么?
- 如何避免拷贝?
- 多重继承时会出现什么状况?如何解决?
- 空类占多少字节?C++编译器会给一个空类自动生成哪些函数?
- 为什么拷贝构造函数必须为引用?
- C++类对象的初始化顺序
- 如何禁止一个类被实例化?
- 为什么用成员初始化列表会快一些?
- 初始化数据成员与对数据成员赋值的含义是什么?有什么区别?
- 静态绑定和动态绑定是怎么实现的?
- 深拷贝和浅拷贝的区别
- 编译时多态和运行时多态的区别
- 实现一个类的成员函数,要求不允许修改类的成员变量。
- 如何让类不能被继承?
- 实例化一个对象需要哪几个阶段
- public,protected及private用法
- 类成员中调用delete this引发的错误解析
- C++成员函数在内存中的存储方式
- C++类内可以定义引用成员么?
什么是虚函数?什么是纯虚函数?
虚函数
被virtual
关键字修饰的成员函数,就是虚函数。
#include <iostream>
using namespace std;
class A{
public:
virtual void v_fun() //虚函数
{
body;
}
}
class B : public A{
public:
void v_fun(){
body;
}
}
int main(){
A* p = new B();
p->v_fun(); //B::v_fun()
return 0;
}
纯虚函数
- 纯虚函数在类中声明时,加上
=0
; - 含有纯虚函数的类称为抽象类(只要含有纯虚函数,这个类就是抽象类),类中只有接口,没有具体的实现方法;
- 继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象;
说明:
- 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
- 可以声明抽象类指针,可以声明抽象类的引用;
- 子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象;
虚函数和纯虚函数的区别
- 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类)
- 使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;
- 定义形式不同:虚函数定义时在普通函数的基础上加上
virtual
关键字,纯虚函数定义时除了加上virtual
关键字还需要加上=0
; - 虚函数必须实现,否则编译器会报错;
- 对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;
- 析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。
哪些函数不能声明为虚函数?
- 普通函数
- 普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写,因此声明为虚函数没有意义,因为编译器会在编译时绑定函数。而多态体现在运行时绑定。通常通过基类指针指向子类对象实现多态。
- 友元函数
- 友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
- 构造函数
- 内联成员函数
- 静态成员函数
- 静态成员函数是编译时确定的,无法动态绑定,不支持多态,因此不能被重写,也就不能被声明为虚函数。
虚函数的实现机制
实现机制
- 虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。
虚函数表相关知识点
- 虚函数表存放的内容:类的虚函数的地址。
- 虚函数表建立的时间:编译阶段,即程序的编译阶段过程中会将虚函数的地址放到虚函数表中。
- 虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
注:虚函数表和类绑定,虚表指针和对象绑定。即类的不同对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。
单继承和多继承的虚函数表结构
编译器处理虚函数表
- 编译器将虚函数的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。
- 如果派生类没有重新定义基类的虚函数A,则派生类的虚函数表中保存的是基类的虚函数A的地址,也就是说基类和派生类的虚函数A的地址是一样的。
- 如果派生类重写了基类的某个虚函数B,则派生类的虚函数表中保存的是重写后的虚函数B的地址,也就是说虚函数B有两个版本,分别存放在基类和派生类的虚函数表中。
- 如果派生类重新定义了新的虚函数C,派生类的虚函数表保存新的虚函数C的地址。
如何禁止构造函数的使用
为类的构造函数增加= delete
修饰符,可以达到虽然声明了构造函数但禁止使用的目的。
#include <iostream>
using namespace std;
class A{
public:
int var1, var2;
A(){
var1 = 10;
var2 = 20;
}
A(int tmp1, int tmp2) = delete;
}
int main(){
A ex1;
A ex2(12, 14); //error
return 0;
}
什么是类的默认构造函数?
默认构造函数:未提供任何实参,来控制默认初始化过程的构造函数称为默认构造函数。
#include <iostream>
using namepace std;
class A{
public:
A(){ //类的默认构造函数
var = 10;
c = 'q';
}
int var;
char c;
}
int main(){
A ex;
cout << ex.c << endl;
cout << ex.var << endl;
return 0;
}
//输出结果 q 10
构造函数,析构函数是否需要定义成虚函数?为什么?
构造函数一般不定义为虚函数,原因:
- 从存储空间的角度考虑:构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此使该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
- 从使用的角度考虑:虚函数是基类的指针指向派生类对象时,通过该指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的。
- 从实现上考虑:虚函数表是在创建对象之后才有的,因此不能定义成虚函数。
- 从类型上考虑:在创建对象时需要明确其类型。
析构函数一般定义成虚函数,原因:
- 析构函数定义成虚函数是为了防止内存泄漏。由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类的析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数没有被声明称虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。所以将析构函数声明为虚函数是十分必要的。
- 在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。
如何避免拷贝?
最直观的想法是:将类的拷贝构造函数和赋值构造函数声明为私有private
,但对于类的成员函数和友元函数依然可以调用,达不到完全禁止类的对象被拷贝的目的,而且程序会出现错误,因为未对函数进行定义。
解决方法:声明一个基类,具体做法如下:
- 定义一个基类,将其中的拷贝构造函数和赋值构造函数声明为
private
- 派生类以私有
private
方式继承基类。
class Uncopyable{
public:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&); //拷贝构造函数
Uncopyable& operator=(const Uncopyable&); //赋值构造函数
}
class A : private Uncopyable //注意继承方式
{
}
简单解释:
- 能够保证,在派生类
A
的成员函数和友元函数中无法进行拷贝操作,因为无法调用基类Uncopyable
的拷贝构造函数或赋值构造函数。同样,在类的外部也无法进行拷贝操作。
多重继承时会出现什么状况?如何解决?
多重继承(多继承):是指从多个直接基类中产生派生类。
多重继承容易出现的问题:命名冲突和数据冗余问题。
举例:
#include <iostream>
using namespace std;
class Base1{
public:
int var1;
};
class Base2 : public Base1
{
public:
int var2;
};
class Base3 : public Base1
{
public:
int var3;
}
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { var1 = tmp; } //error:命名冲突
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
}
上述程序的继承关系是菱形继承
上述代码中存在的问题:
对于派生类Derive
上述代码中存在直接继承和间接继承关系。
- 直接继承:
Base2,Base3
- 间接继承:
Base1
对于派生类中继承的成员变量var1
,从继承关系来看,实际上保存了两份,一份是来自基类Base2
,一份来自基类Base3
,因此出现了命名冲突。
解决方法1: 声明出现冲突的成员变量来源于哪个类
#include <iostream>
using namespace std;
class Base1{
public:
int var1;
};
class Base2 : public Base1
{
public:
int var2;
};
class Base3 : public Base1
{
public:
int var3;
}
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { Base2::var1 = tmp; } //这里声明成员变量来源于类Base2,
void set_var2(int tmp) { var2 = tmp; } //当然也可以声明来源于Base3
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
}
解决方法2: 虚继承
使用虚继承的目的:保证存在命名冲突的成员变量在派生类中只保留一份,即使间接基类中的成员在派生类中只保留一份。在菱形继承关系中,间接基类称为虚基类,直接基类和间接基类之间的继承关系称为虚继承。
实现方式:在继承方式前面加上virtual
关键字。
#include <iostream>
using namespace std;
//间接基类,即虚基类
class Base1{
public:
int var1;
}
//直接基类
class Base2 : virtual public Base1 //虚继承
{
public:
int var2;
}
//直接基类
class Base3 : virtual public Base1 //虚继承
{
public:
int var3;
}
//派生类
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { var1 = tmp; }
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
}
空类占多少字节?C++编译器会给一个空类自动生成哪些函数?
对于空类的声明,编译器不会生成任何的成员函数,只会生成1个字节的占位符。
#include <iostream>
using namespace std;
class A{
empty;
};
int main()
{
cout << "sizeof(A):" << sizeof(A) << endl; //sizeof(A):1
return 0;
}
空类定义时编译器会生成6个成员函数:
当空类A
定义对象时,sizeof(A)
仍是1,但编译器会生成6个成员函数:缺省的构造函数,拷贝构造函数,析构函数,赋值运算符,两个取地址运算符。
#include <iostream>
using namespace std;
/*
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)版本
};
为什么拷贝构造函数必须为引用?
原因:避免拷贝构造函数无限制的递归,最终导致栈溢出。
#include <iostream>
using namespace std;
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)
{
}
};
int main()
{
A ex1(1);
A ex2(2);
A ex3 = ex1;
ex2 = ex1;
ex2.fun(ex1);
return 0;
}
/*
运行结果:
A(int tmp)
A(int tmp)
A(const A &tmp)
A &operator=(const A &tmp)
A(const A &tmp)
*/
-
说明1:
ex2 = ex1;
和A ex3 = ex1;
为什么调用的函数不一样?对象
ex2
已经实例化了,不需要构造,此时只是将ex1
赋值ex2
,只会调用赋值函数;但是ex3
还没有实例化,因此调用的是拷贝构造函数,构造出ex3
,而不是赋值函数,这里涉及到构造函数的隐式调用。 -
说明2:如果拷贝构造函数中形参不是引用类型,
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
,那么又会触发拷贝构造函数,就这样永远的递归下去。 -
说明3:为什么
ex2.fun(ex1)
;会调用拷贝构造函数?因为
ex1
作为参数传递给fun
函数,即A tmp = ex1
;这个过程会调用拷贝构造函数进行初始化。
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()
*/
程序运行结果分析:
- 首先调用基类A和B的构造函数,按照派生列表
public A, public B
的顺序构造; - 然后调用派生类
Test
的成员变量ex1
和ex2
的构造函数,按照派生类中成员变量声明的顺序构造; - 最后调用派生类的构造函数;
- 接下来调用析构函数,和构造函数调用的顺序相反;
如何禁止一个类被实例化?
方法一:
- 在类中定义一个纯虚函数,使该类称为抽象基类,因为不能创建抽象基类的实例化对象。
#include <iostream>
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;
}
方法二:
- 将类的构造函数声明为私有
private
;
为什么用成员初始化列表会快一些?
说明: 数据类型可分为内置类型和用户自定义类型(类类型),对于用户自定义类型,利用成员初始化列表效率高。
原因: 用户自定义类型如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;如果在构造函数中初始化,因为C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,那么在执行构造函数的函数体之前首先调用默认的构造函数为成员变量设初值,在进入函数体之后,调用该成员变量对应的构造函数。因此,使用列表初始化会减少调用默认的构造函数的过程,效率高。
#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()
*/
深拷贝和浅拷贝的区别
如果一个类拥有资源,该类的对象进行复制时,如果资源重新分配,就是深拷贝,否则就是浅拷贝。
- 深拷贝:该对象和原对象占用不同的内存空间,既拷贝存储在栈空间中的内容,又拷贝存储在堆空间中的内容。
- 浅拷贝:该对象和原对象占用同一块内存空间,仅拷贝类中位于栈空间的内容。
当类的成员变量中有指针变量时,最好使用深拷贝。因为当两个对象指向同一块内存空间,如果使用浅拷贝,当其中一个对象删除后,该块内存空间就会被释放,另外一个对象指向的就是无效的内存。
浅拷贝实例
#include <iostream>
using namespace std;
class Test{
private:
int* p;
public:
Test(int temp)
{
this->p = new int(temp);
cout << "Test(int tmp)" << endl;
}
~Test()
{
if(p != NULL)
{
delete p;
}
cout << "~Test()" << endl;
}
};
int main()
{
Test ex1(10);
Test ex2 = ex1;
return 0;
}
说明:上述代码中,类对象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()
*/
编译时多态和运行时多态的区别
编译时多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。
运行时多态:在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。
编译时多态和运行时多态的区别:
- 时期不同:编译时多态发生在程序编译过程中,运行时多态发生在程序的运行过程中;
- 实现方式不同:编译时多态运用泛型编程来实现,运行时多态借助虚函数来实现。
实现一个类的成员函数,要求不允许修改类的成员变量。
如果想达到一个类的成员函数不能修改类的成员变量,只需用const
关键字来修饰该函数即可。
该问题本质是考察const
关键字修饰成员函数的作用,只不过以实例的方式来考察。
#include <iostream>
using namespace std;
class A
{
public:
int var1, var2;
A()
{
var1 = 10;
var2 = 20;
}
void fun() const //不能在const修饰的成员函数中修改成员变量的值,除非该成员变量用 mutable 修饰
{
var1 = 100; // error: assignment of member 'A::var1' in read-only object
}
};
int main()
{
A ex1;
return 0;
}
如何让类不能被继承?
解决方法一:借助final
关键字,用该关键字修饰的类不能被继承。
#include <iostream>
using namespace std;
class Base final {};
class Derive : public Base {} //error
解决方法二:借助友元,虚继承和私有构造函数来实现。
#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
类的构造函数是private
的,C
类没有权限访问,因此,无法创建C
类的对象,B
类是不能被继承的类。
注意:在继承体系中,友元关系不能被继承,虽然C
类继承了B
类,B
类是Base
类的友元函数,但是C
类和Base
类没有友元关系。
为什么必须是虚继承呢?
- 通常每个类只初始化自己的直接基类,但是在虚继承的时候这个情况发生了变化,可能导致虚基类被多次初始化,这显然不是我们想要的。(例2:AA,AB都是类A的派生类,然后类C又继承自AA和AB,如果按之前的方法会导致C里面A被初始化两次,也会存在两份数据)。
- 为了解决重复初始化的问题,从具有虚基类的类继承的类在初始化时进行了特殊处理,在虚派生中,由最低层次的派生类的构造函数初始化虚基类。在上面就是由C的构造函数控制虚基类的初始化。
Base
类的构造函数是private
的,C
类没有权限访问,因此,无法创建C
类的对象,B
类是不能被继承的类。
注意:在继承体系中,友元关系不能被继承,虽然C
类继承了B
类,B
类是Base
类的友元,但是C
类和Base
类没有友元关系。
实例化一个对象需要哪几个阶段
-
分配空间
创建类对象首先要为该对象分配内存空间。不同的对象,为其分配空间的时机未必相同。全局对象,静态对象,分配在栈区域的对象,在编译阶段进行内存分配;存储在堆空间的对象,是在运行阶段进行内存分配。
-
初始化
首先明确一点:初始化不同于赋值,初始化发生在赋值之前,初始化随对象的创建而进行,而赋值是在对象创建好后,为其赋上相应的值。初始化列表先于构造函数体内的代码执行,初始化列表执行的是数据成员的初始化过程。
-
赋值
对象初始化完成之后,可以对其进行赋值,对于一个类的对象,其成员变量的赋值过程发生在类的构造函数的函数体中。当执行完该函数体,也就意味着类对象的实例化过程完成了。总结:构造函数实现了对象的初始化和赋值两个过程,对象的初始化时通过初始化列表来完成,而对象的赋值则才是通过构造函数的函数体来实现。
-
注:对于拥有虚函数的类的对象,还需要给虚表指针赋值。
没有继承关系的类,分配完内存后,首先给虚表指针赋值,然后再列表初始化以及执行构造函数的函数体。
有继承关系的类,分配内存之后,首先进行基类的构造过程,然后给派生类的虚表指针赋值,最后再列表初始化以及执行构造函数的函数体。
public,protected及private用法
用户代码(类外)可以访问public
的成员而不能访问private
成员;private
成员只能由类成员(类内)和友元访问。
protected
成员可以被派生类对象访问,不能被用户代码(类外)访问。
class A {
public:
int a;
A() {
a1 = 1;
a2 = 2;
a3 = 3;
a = 4;
}
void fun() {
cout << a << endl;
cout << a1 << endl;
cout << a2 << endl;
cout << a3 << endl;
}
public:
int a1;
protected:
int a2;
private:
int a3;
};
int main()
{
A itema;
itema.a = 10; //正确
itema.a1 = 20; //正确
itema.a2 = 30; //错误,类外不能访问protected
itema.a3 = 40; //错误,类外不能访问private成员
return 0;
}
继承中的特点:
不管是否继承,上面的规则永远适用。
- public继承:基类
public
成员,protected
成员,private
成员的访问属性在派生类中分别变成public
,protected
,private
; - protected继承:基类
public
成员,protected
成员,private
成员的访问属性在派生类中分别变成protected
,protected
,private
; - private继承:基类
public
成员,protected
成员,private
成员的访问属性在派生类中分别变成private
,private
,private
;
无论哪种继承方式,上面两点都没有改变
private
只能被本类成员(类内)和友元访问,不能被派生类访问;protected
成员可以被派生类访问;
类成员中调用delete this引发的错误解析
在类的成员函数中能不能调用delete this
。
答案是肯定的,能调用!
如果是通过类对象来调用一个带有delete this
的成员函数,则后续不能再次调用该对象的其他方法。如果是通过指向对象的指针来调用一个带有delete this
的成员函数,则后续可以再次调用该对象的其他方法,只不过被调用的方法不涉及到这个对象的数据成员和虚函数。
当一个类对象声明时,系统会为其分配内存空间。
在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含成员函数的代码,类的成员函数单独放在代码段中。
当调用delete this
时,类对象的内存空间被释放。
在delete this
之后进行的其他任何函数调用,只要不涉及到this
指针的内容,都能正常运行。一旦涉及到this
指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。
delete this
释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统回收。此时这段内存是可以访问的,但是其中的值是不确定的。
如果在类的析构函数中delete this
,会发生什么?
这会导致堆栈溢出。原因很简单,delete
的本质是调用析构函数,然后释放内存。显然,delete this
会去调用本对象的析构函数,而析构函数又调用delete this
,形成无限递归,造成堆栈溢出,系统崩溃。
总结:
- 在成员函数中调用
delete this
,会导致指针错误,而在析构函数中调用delete this
,会出现死循环,造成堆栈溢出。
有一种情况下必须显式使用this
:当我们需要将一个对象作为整体引用而不是引用对象的一个成员时。
C++成员函数在内存中的存储方式
每个对象所占的存储空间只是该对象的数据部分(虚表指针也属于数据部分)所占用的存储空间,而不包括函数代码所占用的存储空间。
class D
{
public:
void printA()
{
cout<<"printA"<<endl;
}
virtual void printB()
{
cout<<"printB"<<endl;
}
};
int main(void)
{
D *d=NULL;
d->printA(); //输出printA
d->printB(); //出错
}
类的静态成员函数和非静态成员函数的区别:静态成员函数和非静态成员函数都是在类的定义时存放在内存的代码区的,它们都是属于类的。但是类为什么只能调用静态类成员函数,而非静态类成员函数(即使没有参数)只有类对象才能调用?原因是类的非静态成员函数其实都内含了一个指向类对象的指针型参数(this指针),因此只有类对象才能调用(此时this指针有实值)。
上面的程序在输出“printA”后,程序崩溃。类中包括成员变量和成员函数。new
出来的只是成员变量,成员函数始终存在,所以如果成员函数未使用任何成员变量的话,不管是不是static
,都能正常工作。需要注意的是,虽然调用不同对象的成员函数时都是执行同一段函数代码,但是执行结果一般是不相同的。不同的对象使用的是同一个函数代码段,它怎么能分别对不同对象中的数据进行操作呢?C++为此专门设立了一个名为this
的指针,用来指向不同的对象。
需要说明,不论成员函数在类内定义还是类外定义,成员函数的代码段都用同一种方式存储。不要将成员函数的这种存储方式和inline
函数的概念混淆。不要误以为用inline
声明(或默认为inline
)的成员函数,其代码段占用对象的存储空间,而不用inline
声明的成员函数,其代码段不占用对象的存储空间。不论是否用inline
声明(或默认为inline
),成员函数的代码段都不占用对象的存储空间。用inline
声明的作用是在调用该函数时,将函数的代码段复制插入到函数调用点,而若不用inline
声明,在调用该函数时,流程转去函数代码段的入口地址,在执行完该函数代码段后,流程返回函数调用点。inline
与成员函数是否占用对象的存储空间无关,它们不属于同一个问题。
C++类内可以定义引用成员么?
类成员变量可以定义为引用类型。
在类中定义引用变量,必须要在初始化列表中初始化该成员变量(const
类型数据成员也必须在初始化列表中进行初始化)。