目录
先看一个完整的示例,再逐一说明要点:
#include <iostream>
//类定义
class ClassA
{
private:
//类属性
int a=1;
double b;
//非const的静态属性不能再类定义时初始化
static int c;
public:
//构造函数
ClassA(int i);
//如果添加了其他构造函数,则需要手动添加默认构造函数
ClassA();
//析构函数
~ClassA();
//类函数,const关键字表示该方法不会修改调用对象属性
void print() const;
static int add(int i);
};
//在类函数定义前必须显示初始化静态属性,否则报错静态属性未引用
int ClassA::c=0;
ClassA::ClassA(){
}
ClassA::ClassA(int i){
a=i;
b=2;
}
ClassA::~ClassA(){
}
void ClassA::print() const {
//非静态方法可以访问静态成员
std::cout<<"a="<< a <<",b="<< b <<",c="<<c<< std::endl;
}
//静态函数的定义前不能加static关键字
int ClassA::add(int i){
//编译报错,访问非静态成员
// b+=i;
c+=i;
return c;
}
int main(){
//通过类名调用静态函数
ClassA::add(10);
//自动调用默认构造函数,跟下面显示调用等价
// ClassA a;
// ClassA a=ClassA();
ClassA a{};
a.add(1);
a.print();
//下面三种调用方式等价
// ClassA b=ClassA(2);
// ClassA b(2);
ClassA b{2};
b.add(2);
b.print();
return 0;
}
一、访问控制private/public
类属性和函数默认是private,只有类内部才能访问,如果是public则可以通过类对象或者类名来访问,Java中默认是包级访问。
二、构造函数和析构函数
构造函数是用来初始化类属性的特殊类函数,没有返回值,跟普通函数一样可以定义参数默认值,可以重载。析构函数是当对象销毁时自动执行的一个特殊类函数,没有返回值,也没有入参,主要用于清理创建对象时使用的资源,如new分配的内存。如果对象是自动存储期的,则在方法退出时调用析构函数,如果是静态存储期的,则在进程结束时调用,如果是动态存储期,则在delete时调用。
当类中未定义构造函数和析构函数时,编译器会自动添加默认的构造函数和析构函数,默认情况下都是空实现,默认构造函数没有入参,如果类中定义了则不添加。部分场景下如对象数组初始化,只使用没有入参的构造函数,这时需要手动添加入参为空的构造函数。
#include <iostream>
class ClassB
{
private:
int a=1;
public:
//构造函数
ClassB(int i);
//析构函数
~ClassB();
};
ClassB::ClassB(int i){
a=i;
}
ClassB::~ClassB(){
std::cout<<"~ClassB a="<< a << std::endl;
}
//静态存储期变量,main方法退出即进程结束前调用析构函数
ClassB a(1);
//编译报错,缺少实参,即在程序添加了构造函数后,编译器不会自动添加默认无参构造函数
//ClassB a2;
int main(){
//大括号表示这是一个代码块,当执行完成就按照变量分配的顺序倒序销毁自动存储变量
{
//自动存储期变量
ClassB b(2);
ClassB b2(22);
ClassB b3(23);
//动态存储期变量,按照delete的顺序执行析构函数
ClassB * c=new ClassB(3);
ClassB * c2=new ClassB(32);
ClassB * c3=new ClassB(33);
delete c;
delete c2;
delete c3;
}
std::cout<<"exit\n";
return 0;
}
三、静态成员
普通的类属性或者函数都是非静态成员,跟Java一样,只能通过类对象实例访问;如果在类属性或者函数前加static关键字则表示静态属性或者静态函数,可以通过类名或者类对象实例访问。静态属性是所有类对象实例共享的,在静态存储区保存,同Java,静态函数内不能访问非静态成员,非静态函数可以访问静态成员。无论方法是否是静态的,方法对应的二进制机器码在内存中只有一份。注意非const的静态属性不能再类定义时初始化,必须在类实现时显示初始化,否则报错静态属性未引用;定义静态函数时前面不需要加static关键字,否则报错不能将成员函数声明为有静态链接。
四、类属性默认值
默认构造函数不会初始化类属性,编译器不会像Java一样提供默认值,这时程序获取类属性的值是对应内存的原始值,结果不确定。C++98中只有静态的const类属性才能在声明时用=赋值,在程序加载的时候自动初始化,C++11在基础上允许非静态属性在声明时使用=指定默认值,注意构造函数中的赋值操作会覆盖默认值。
#include <iostream>
class ClassC
{
private:
int a=1;
double b;
//非const的静态属性不能再类定义时初始化
static int c;
static const int d=1;
public:
ClassC(int i);
void print();
};
int ClassC::c=1;
ClassC::ClassC(int i){
a=i;
b=2;
}
void ClassC::print(){
std::cout<<"a="<< a <<",b="<< b <<",c="<<c<<",d="<<d<< std::endl;
}
int main(){
ClassC c(2);
c.print();
return 0;
}
五、this指针
构造函数和类成员函数是如何访问当前对象的属性值了?答案是this指针,指向当前构造对象或者调用对象,通常作为函数隐含的入参传入到函数中,跟Java一样。代码中也可显示使用this指针,如下:
#include <iostream>
class ClassD
{
private:
int a=1;
int * b;
int c;
int d;
public:
ClassD(int * i,int j,int k);
void print();
};
void ClassD::print(){
int sum=this->a+(*this->b)+this->c+this->d;
std::cout<<"sum="<<sum << std::endl;
}
ClassD::ClassD(int * i,int j,int k){
this->b=i;
this->c=j;
this->d=k;
}
int main(){
int a=1,b=2,c=3;
ClassD d(&a,b,c);
d.print();
return 0;
}
六、初始化列表
初始化列表支持在对象构造时直接对对象属性赋值,同正常的构造函数内初始化相比,最大的区别是如果类属性是用户自定义的类类型,则可以直接利用入参对类属性赋值,不需要先调用类属性的无参构造函数完成初始化,两者的区别相当于初始化列表执行int &a =b, 而构造函数初始化执行int &a; a=b。初始化列表和构造函数赋值对基本数据类型和指针没区别,对用户自定义类类型成员变量,为了避免类成员变量无参构造函数的调用,推荐使用初始化列表。但有的时候必须用带有初始化列表的构造函数:
- 成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
- const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。
- 子类显示调用父类的非默认构造函数完成父类属性初始化
6.1 基本数据类型和指针下的两者的反汇编代码
#include <iostream>
class ClassD
{
private:
int a=1;
int * b;
int c;
int d;
public:
ClassD(int * i,int j,int k);
void print();
};
//列表初始化,表示在对象构造阶段用i来初始化属性b,j初始化属性c,k初始化属性d
//效果等同于下面的构造函数内赋值
//ClassD::ClassD(int * i,int j,int k):b(i),c(j),d(k) {
//
//}
void ClassD::print(){
int sum=a+(*b)+c+d;
std::cout<<"sum="<<sum << std::endl;
}
ClassD::ClassD(int * i,int j,int k){
b=i;
c=j;
d=k;
}
int main(){
int a=1,b=2,c=3;
ClassD d(&a,b,c);
d.print();
return 0;
}
使用初始化列表时汇编代码如下:
movl $1, -12(%rbp)
movl $2, -4(%rbp)
movl $3, -8(%rbp)
movl -8(%rbp), %ecx
movl -4(%rbp), %edx
leaq -12(%rbp), %rsi
leaq -48(%rbp), %rax
movq %rax, %rdi 上述代码将执行构造函数的参数在栈中初始化,然后放入寄存器中,第一个参数即rdi中的值是默认传递到函数中的指向当前调用对象的this指针,即即将构造的对象的内存地址,rsi,edx,ecx依次是函数声明的三个入参。接着进入构造函数,如下:
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movl %edx, -20(%rbp)
movl %ecx, -24(%rbp) 将传入函数的4个参数依次放入栈中
movq -8(%rbp), %rax 将this指针放入rax寄存器
movl $1, (%rax) 属性a赋值
movq -8(%rbp), %rax
movq -16(%rbp), %rdx
movq %rdx, 8(%rax) 属性b赋值
movq -8(%rbp), %rax
movl -20(%rbp), %edx
movl %edx, 16(%rax) 属性c赋值
movq -8(%rbp), %rax
movl -24(%rbp), %edx
movl %edx, 20(%rax) 属性d赋值,利用this指针依次对对象属性赋值,先将值从栈拷贝到寄存器,再从寄存器拷贝到对象属性对应内存区
将初始化列表代码注掉,使用构造函数赋值时汇编语句跟上面一样,从而证明使用初始化列表和构造函数赋值对基本数据类型和指针没区别。
6.2 程序自定义类类型下的两者的反汇编代码
class ClassA{
private :
int a;
public:
//如果无参构造函数注掉,报错no matching function for call to ‘ClassA::ClassA()’
ClassA();
ClassA(int a);
int getA();
};
class ClassD
{
private:
//如果属性a改成ClassA的引用类型,则报错‘class ClassA&’中有未初始化的引用成员
//ClassA & a;
ClassA a;
//如果属性b前加const,则报错‘const int ClassD::b’ should be initialized
//const int b;
int b;
public:
ClassD(ClassA & i,int j);
void print();
};
ClassA::ClassA(){
}
ClassA::ClassA(int a){
this->a=a;
}
int ClassA::getA(){
return a;
}
void ClassD::print(){
int sum=a.getA()+b;
std::cout<<"sum="<<sum << std::endl;
}
ClassD::ClassD(ClassA & i,int j){
a=i;
b=j;
}
//ClassD::ClassD(ClassA & i,int j):a(i),b(j){
//}
int main(){
ClassA a(1);
ClassD d(a,2);
d.print();
return 0;
}
使用构造函数初始化的汇编代码如下:
leaq -16(%rbp), %rax
movl $1, %esi
movq %rax, %rdi 将ClassA构造函数的参数放入寄存器中,rdi中的是this指针
然后进入ClassA的构造函数:
movq %rdi, -8(%rbp)
movl %esi, -12(%rbp)
movq -8(%rbp), %rax
movl -12(%rbp), %edx
movl %edx, (%rax) ClassA 的属性a赋值
ClassA的构造函数退出后:
leaq -16(%rbp), %rcx 将ClassA的实例a的地址拷贝到rcx中
leaq -32(%rbp), %rax 将ClassD的实例d的地址拷贝到rax中
movl $2, %edx
movq %rcx, %rsi
movq %rax, %rdi 将ClassD构造函数的参数放入寄存器中
然后进入ClassD的构造函数:
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movl %edx, -20(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi 将ClassD构造函数的参数从寄存器中读取出来放入栈中
然后进入ClassA的无参构造函数,初始化ClassD的a属性
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movl $0, (%rax) 初始化ClassA的属性a
ClassA的无参构造函数退出后:
movq -8(%rbp), %rax
movq -16(%rbp), %rdx
movl (%rdx), %edx
movl %edx, (%rax) 将ClassD构造函数的入参i赋值给ClassD的属性a
movq -8(%rbp), %rax
movl -20(%rbp), %edx
movl %edx, 4(%rax) 将ClassD构造函数的入参j赋值给ClassD的属性b, 构造完成。
再看看使用初始化列表下的汇编代码,区别主要在ClassD的构造函数中,执行ClassD构造函数的代码如下:
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movl %edx, -20(%rbp)
movq -8(%rbp), %rax
movq -16(%rbp), %rdx
movl (%rdx), %edx
movl %edx, (%rax)
movq -8(%rbp), %rax
movl -20(%rbp), %edx
movl %edx, 4(%rax)
与使用构造函数初始化的汇编代码相比,唯一的区别就是少了一次ClassA的无参构造函数的调用。
6.3 两者初始属性的顺序问题
#include <iostream>
class ClassD
{
private:
int a=1;
int * b;
int c;
int d;
public:
ClassD(int * i,int j,int k);
void print();
};
ClassD::ClassD(int * i,int j,int k):b(i),c(j),d(k) {
}
//ClassD::ClassD(int * i,int j,int k):d(k),b(i),c(j){
//
//}
void ClassD::print(){
int sum=a+(*b)+c+d;
std::cout<<"sum="<<sum << std::endl;
}
//ClassD::ClassD(int * i,int j,int k){
// b=i;
// c=j;
// d=k;
//}
//
//ClassD::ClassD(int * i,int j,int k){
// c=j;
// d=k;
// b=i;
//}
int main(){
int a=1,b=2,c=3;
ClassD d(&a,b,c);
d.print();
return 0;
}
比较两种列表初始化下的汇编代码,结果一样,都是按照类属性声明的顺序初始化,如下 :
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movl %edx, -20(%rbp)
movl %ecx, -24(%rbp)
movq -8(%rbp), %rax
movl $1, (%rax) 初始化属性a
movq -8(%rbp), %rax
movq -16(%rbp), %rdx
movq %rdx, 8(%rax) 初始化属性b
movq -8(%rbp), %rax
movl -20(%rbp), %edx
movl %edx, 16(%rax) 初始化属性c
movq -8(%rbp), %rax
movl -24(%rbp), %edx
movl %edx, 20(%rax) 初始化属性d
比较两种构造函数初始化的汇编代码,两者不一样,按照构造函数中属性赋值的实际顺序初始化,以第二种构造函数初始化为例:
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movl %edx, -20(%rbp)
movl %ecx, -24(%rbp)
movq -8(%rbp), %rax
movl $1, (%rax) 初始化属性a
movq -8(%rbp), %rax
movl -20(%rbp), %edx
movl %edx, 16(%rax) 初始化属性c
movq -8(%rbp), %rax
movl -24(%rbp), %edx
movl %edx, 20(%rax) 初始化属性d
movq -8(%rbp), %rax
movq -16(%rbp), %rdx
movq %rdx, 8(%rax) 初始化属性b
七、类内联函数
上述示例中class ClassD { }的部分就是类定义,类定义中通常包含类属性和类函数的声明,一般将类定义放在头文件中,类函数的定义或实现放在另一个单独文件中。类定义中也能定义函数,但是这种函数自动视为内联函数。
类函数定义时需要在函数名前面加上类名,如示例中的void ClassA::print(),类名标识当前方法所属的类或者类作用域,其中ClassA::print称为方法的限定名,print是方法的缩写名,注意如果是静态函数则函数定义前不需要加static。因为类作用域的存在,允许同一个文件中存在属于不同的类的同名方法。
头文件 test.h:
class ClassA
{
private:
int a=1;
double b;
public:
void print();
//类定义中的函数定义默认为内联函数
int getA(){
return a;
}
};
class ClassB
{
private:
int a=2;
double b;
public:
void print();
int getB();
};
//显示定义内联函数,因为内联函数要求在使用的地方都有定义,所以通常将其放在头文件中
inline int ClassB::getB(){
return b;
}
类函数实现:
#include <iostream>
#include "test.h"
void ClassA::print(){
int a=getA();
std::cout<<"ClassA,a="<< a <<",b="<< b << std::endl;
}
void ClassB::print(){
int b=getB();
std::cout<<"ClassB,a="<< a <<",b="<< b << std::endl;
}
int main(){
ClassA a;
ClassB b;
a.getA();
b.getB();
a.print();
b.print();
return 0;
}
怎样证明是内联函数了?反汇编,执行a.getA()时,如下:
执行b.getB()时,如下:
两个都执行了callq指令,所以可以确定这不是内联函数,哪出问题了?因为代码中的内联函数对编译器而言只是一种建议,最终是否编译成内联函数由编译器判断,没有加inline关键字的也可能被编译成内联函数。GNU中可通过增加__attribute__((always_inline))强制编译器使用内联函数。更新后的test.h如下:
class ClassA
{
private:
int a=1;
double b;
public:
void print();
//类定义中的函数定义默认为内联函数
__attribute__((always_inline)) int getA(){
return a*2;
}
};
class ClassB
{
private:
int a=2;
double b;
public:
void print();
int getB();
};
//显示定义内联函数,因为内联函数要求在使用的地方都有定义,所以通常将其放在头文件中
__attribute__((always_inline)) inline int ClassB::getB(){
return b*2;
}
重新编译反汇编,发现a.getA();b.getB(); 这两行直接跳过,因为进行内联函数替换后这两行是无意义的,进入a.print(),执行到getA()时,直接跳转到getA()的函数定义处,没有callq指令:
执行getB()时,结果一样,如下图:
inline之__attribute__((always_inline))
八、 类的内存表示
类函数中的类其实是编译层面的概念,由编译器支持并实现的,在编译成汇编代码后,类函数跟普通函数一样,变成汇编语言中的方法,即callq指令的调用对象。综合上面类属性默认值和初始化列表的汇编分析可以得知,类在内存中跟结构变量是一样的,在内存中就是一片连续的内存区域,这片区域按照类属性声明的顺序依次保存着各个属性,注意为了方便整体赋值时内存拷贝方便,类在内存中的大小按照8字节对齐,8字节是64位寄存器的容量。类对象实例在内存中并没有保存指向各类函数的指针,类对象实例对类函数的调用在编译环节会直接转换翻译成对对应的函数的调用。
#include <iostream>
class ClassA
{
private:
int a=1;
double b;
public:
void print();
};
class ClassB
{
private:
int a=1;
ClassA c;
public:
void print();
void print2();
};
class ClassC
{
private:
double a=1;
ClassA c;
public:
void print();
void print2();
void print3();
};
void ClassA::print(){
std::cout<<"ClassA,a="<< a <<",b="<< b << std::endl;
}
void ClassB::print(){
std::cout<<"ClassA,a="<< a << std::endl;
}
void ClassB::print2(){
std::cout<<"ClassA,a="<< a << std::endl;
}
void ClassC::print(){
std::cout<<"ClassA,a="<< a << std::endl;
}
void ClassC::print2(){
std::cout<<"ClassA,a="<< a << std::endl;
}
void ClassC::print3(){
std::cout<<"ClassA,a="<< a << std::endl;
}
int main(){
//获取内存大小
std::cout <<"int size:"<< sizeof(int) <<",double size:" << sizeof(double) <<"\n";
std::cout <<"ClassA size:"<< sizeof(ClassA)
<<",ClassB size:" << sizeof(ClassB)
<<",ClassC size:" << sizeof(ClassC)
<<"\n";
//获取内存对齐的字节数,C中是_Alignof,GNU中两者不兼容
std::cout <<"ClassA _Alignof size:"<< __alignof(ClassA)
<<",ClassB _Alignof size:" << __alignof(ClassB)
<<",ClassC _Alignof size:" << __alignof(ClassC)
<<"\n";
ClassA a;
ClassA a2;
ClassA a3;
a.print();
a2.print();
a3.print();
return 0;
}
上述示例的运行结果如下图:
ClassA的属性b未显示初始化,所以值是不确定的,如上诉第三个b。ClassA中两个类属性是8+4=12,按照8字节对齐就是16;ClassB的两个类属性是4+16=24,8字节对齐是24;ClassC的两个类属性是8+16=24,符合对齐要求。ClassC中声明了3个类函数,类大小依然是24,说明类实例在内存中并未保存指向类函数的指针。
再反汇编上述代码,查看a,a2,a3的调用代码:
ClassA的三个实例调用print方法都是调用同一个,证明类函数编译成汇编语言后跟普通的函数一样。