1、类对象模型
(1)如何计算类对象大小
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类大小呢?
- 一个类对象包含了什么:类的成员变量 、 类的成员函数
- 要知道如何计算类的大小,先来要知道一个类对象是如何存储的。有两种对象存储方式:对象中包含类的各个成员 、只保存成员变量,成员函数存放在公共的代码段
方式一: 对象中包含类的各个成员
注意: 每个对象中成员变量是不同的,但是调用的是同一份函数,按照这种方式存储,当一个类创建多个对象时,那就导致相同的代码被保存多次,浪费空间。
方式二:只保存成员变量,成员函数存放在公共的代码段
注意: 这种方法同上面那种方法想比较,一个类中成员变量是不同的,但是调用的是同一份成员函数,那我们可以换一种思路,将成员函数单独存放在一块空间之中也就是:公共代码区,只需要在类实例化对象时只需要存储成员变量即可,这样就节省空间同时提高效率
综上我们就可以得出一个结论:一个类的大小就是该类中“成员变量”之和,当然也要进行内存对齐[详解],注意空类的大小,空类比较特殊,编译器给空类一个字节来唯一标识这个类。
#include<iostream>
#include<windows.h>
using namespace std;
class A
{
public:
void PrintA()//成员函数
{
cout<<_a<<endl;
}
private:
char _a;//成员变量
};
int main()
{
cout<<sizeof(A)<<endl;//此时输出1,因为成员变量_a是char类型,占一个字节;
system("pause");
}
2、this指针
(1)先来看一个简单的日期类
class Date
{
public:
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void setDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;//年
int _month;//月
int _day;//日
};
int main()
{
Date d1, d2;
d1.setDate(1990, 1, 1);
d2.setDate(2018, 7, 1);
d1.Display();
d2.Display();
system("pause");
return 0;
}
可以看出Date类中有两个成员函数,应该是显示日期的Display和设置日期的setDate,函数体重也没有关于不同对象的区分,那么当s1调用setDate函数时,函数是如何知道应该设置s1对象,而不是设置s2对象呢?
所以在c++中,引入了this指针来解决该问题,就业就是:c++编译器个每一个成员函数增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户来说都是透明的,意思就是用户不用刻意的去传递这个参数,编译器会自动完成。
(2)this指针的特性
- this指针的类型:类类型 const*
- 只能在成员函数的内部使用
- this指针本质上就是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
- this指针是成员函数第一个隐含的指针形参,一般情况有编译器通过ecx寄存器自动传递,不需要用户传递
有两问题:(1):this指针能为空吗?(2):this指针存储在哪里?
(1)this可以为空,当我们在调用函数的时候,如果函数内部并不需要使用到this,也就是不需要通过this指向当前对象并对其进行操作时才可以为空(当我们在其中什么都不放或者在里面随便打印一个字符串),如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用)就跟C中一样不能进行空指针的引用
(2)其实编译器在生成程序时加入了获取对象首地址的相关代码。并把获取的首地址存放在了寄存器ECX中(VC++编译器是放在ECX中,其它编译器有可能不同)。也就是成员函数的其它参数正常都是存放在栈中。而this指针参数则是存放在寄存器中。类的静态成员函数因为没有this指针这个参数,所以类的静态成员函数也就无法调用类的非静态成员变量。
class Date
{
public:
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void setDate(int year = 1990,int month = 1,int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
printf("hello this!!!\n");
}
private:
int _year;//年
int _month;//月
int _day;//日
};
int main()
{
Date* p = NULL;
p->Print();//输出 hello this!!!
p->Display();//程序崩了
system("pause");
return 0;
}
3、引用
(1)概念:引用不是新定义一个变量,而是给已经存在的变量取一个别名,编译器不会为了引用变量开辟空间,它和它的引用的变量共用同一块内存空间
类型& 应用变量名(对象名) = 引用实体
void Print()
{
int a = 10;
int& ra = a;//定义了引用
printf("%p\n",&a);//输出010FF8E4
printf("%p\n",&ra);//输出010FF8E4
}
注意:引用类型必须和引用实体是同种类型的
(2)引用特性
- 应用在定义是必须初始化
- 一个变量可以有多个引用,(就像一个人可以有多个外号)
- 引用一旦引用了一个实体,就不能再引用其他实体(专一性)
void test()
{
int a = 10;
//int& ra; //语句会报错,没有初始化
//体现一个变量可以有多个引用
int& ra = a;
int& rra = a;
const int b = 10;
//int& rb = b;//编译时会报错,因为b为常量
const int& rb = b;
//const double& rb = b; //会报错类型不符合
}
(3)使用场景:(1)做参数 (2)做返回值
做参数:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
做返回值
int& TestRefReturn(int& a)
{
a += 10;
return a;
}
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl; //输出为7
return 0;
}
注意:如果函数返回值,离开函数作用域后,其栈上的空间已经还给了系统,因此不能用栈上的空间作为引用类型返回。所以以引用类型返回,返回值的生命周期必须不受函数的限制(返回值的生命周期必须比函数的生命周期长)
(4)传值和传引用的效率比较
传值和传引用作为参数时和返回值:
struct A
{
int a[100000];
};
A a;
A test1()//传值做返回值
{
return a;
}
void test2(A a)//传值作为参数
{
}
A& test3()//传引用作为返回值
{
return a;
}
void test4(A& a)//传引用作为参数
{
}
void test()
{
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
{
test1(a);
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
{
test3(a);
}
size_t end2 = clock();
size_t begin3 = clock();
for (size_t i = 0; i < 100000; ++i)
{
test2(a);
}
size_t end3 = clock();
size_t begin4 = clock();
for (size_t i = 0; i < 100000; ++i)
{
test4(a);
}
size_t end4 = clock();
cout << "传值作为返回值test1-time:" << end1 - begin1 << endl;
cout << "传引用作为返回值test3-time:" << end2 - begin2 << endl;
cout << "传值作为参数test2-time:" << end3 - begin3 << endl;
cout << "传引用作为参数test4-time:" << end4 - begin4 << endl;
}
int main()
{
test();
system("pause");
return 0;
}
运行结果:
结论:传值和传传引用在作为传参以及返回值类型上效率相差很大。
(5)应用和指针的区别
理论上应用就是一个别名,和引用实体共用同一块空间,所以二者的地址都是相同的。
int main()
{
int a = 10;
//引用
int& ra = a;
ra = 20;
//指针
int* pa = &a;
*pa = 20;
return 0;
}
来看一下反汇编代码:
引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在引用一个实体后,就不能再引用其他实体,指针却可以在任何时候指向任何一个同一个类型的实体
- 引用没有NULL,指针由NULL
- 在sizeof中含义不同:引用结果为引用类型大小,指针始终都是地址空间所占字节个数(32为下大小为4字节)
- 引用自加就是引用实体增加1,而指针向后偏移一个类型大小
- 有多级指针,没有多级引用
- 访问方式不同:指针要解引用,引用编译器自己处理
- 引用比指针用起来安全
如有见解,请不吝赐教,感激