了解面向过程和面向对象:C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
C语言中我们通过结构体将变量进行封装,而结构体中只能定义变量不能定义函数。在C++中,结构体不仅可以定义变量也可以用来定义函数,而C++中更习惯将这样的结构体称为“类”,使用关键字class来进行定义,为了兼容C语言还保留了结构体的用法。
类的定义
class className{
//成员变量、成员方法
}; //注意,这里的分号不能省略(和结构体相同)
成员变量:类中定义的变量称为类的成员变量或类的属性。
成员方法:类中定义的函数称为成员函数或成员方法。
注意:1.类的声明和定义如果全部放在类体中,如果成员方法也在类中定义,编译器会将成员方法当成内联函数处理;
2.类还可以先声明,在定义。声明在.h文件中,只需要对类中的成员变量和成员方法进行声明;定义在.cpp文件中,对类中的成员方法进行定义(实现)。
2.1 访问限定符
C++中通过类将对象的属性和方法进行封装,让其更加完善,同时使用访问限定符将对象中的方法和变量选择性的提供给用户。
三种访问限定符
public(公有):使用public修饰的成员变量或成员方法,在类外可以随意访问。
private(私有):使用private修饰的成员变量或成员方法,只能在类中使用。
protected(保护):使用private修饰的成员变量或成员方法,只能在类中使用。
注意:访问限定符的作用域为从该访问权限符出现的位置到下一个访问权限符出现的位置;在类中(class定义的)默认访问限定符为private,在结构体中默认为public。
面试题:C++中struct和class的区别是什么?
答案:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类,和class定义类是一样的,区别是struct的成员默认访问方式是public,classt的成员默认访问方式是private。
2.2 封装
面向对象的三大特征:封装、继承、多态
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
3.1 类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域
//.h文件声明类
class student{
//成员方法
void showInfo();
//成员变量
int _age;
char name[20];
}
//.cpp文件中定义类中的方法
void student::showInfo(){ //使用::作用域解析符指名类
cout<<"name:"<
}
3.2 类的实例化
类是一个类型,不能直接使用,通常需要使用类类型创建对象,对象调用类中的方法。而用类类型创建对象的过程就称为类的实例化。
1)类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
2)一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
3)类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
示例:
int main(){
Date date;//使用Date类实例化一个对象
date.getDay();//通过实例化出的对象调用方法
return 0;
}
4.1 类的对象模型
4.1.1 类的大小的计算
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如果类的大小是类中成员变量和成员函数的大小之和,那么为什么上述示例中类的大小只有12?如何计算类的大小呢?
4.1.2 类对象的存储方式
如果使用类创建的对象中将类中的成员变量和成员方法都进行存储,每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。因此,对象只保存成员变量而成员方法保存在公共代码段中。
结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
面试题:定义一个空的类型,里面没有任何的成员变量和成员函数,对该类型求sizeof,结果是多少?
答案:1
追问:为什么不是0?
答案:空类型的实例中不包含任何信息,本来求sizeof应该是0,但是当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。Visual Studio中每个空类型的实例占用1字节的空间。
追问:如果在该类型中添加一个构造函数和析构函数,再对该类型求sizeof,得到的结果又是多少?
答案:和前面一样,还是1。调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不会因为这两个函数而在实例内添加任何额外的信息。
追问:那如果把析构函数标记为虚函数呢?
答案:C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4字节的空间,因此求sizeof得到4;如果是64位的机器,一个指针占8字节的空间,因此求sizeof则得到8。
4.1.3 结构体内存对齐规则
在结构体中有一个重要的知识,就是结构体内存对齐,也是很多公司笔试常考的一个知识点。只有掌握结构体内存对齐,才能学会计算结构体大小。结构体类型不同于其他类型,结构体类型是一种重要的自定义类型,同时结构体类型大小计算也不同于其他类型,结构体类型大小不是直接将结构体成员变量类型之和,在计算结构体大小时最重要的一个知识就是内存对其。
什么是内存对齐?结构体内存对齐的规则是什么?
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(VS中默认的值为8)。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举个例子:
为什么存在内存对齐?
大部分的参考资料都是如是说的:
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的 内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
利用上面的内存对其知识计算下面两个结构体的大小,你发现了什么问题?
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
经过计算我们会发现,两个结构体的大小不一样,结构体S1的大小为12,结构体2的大小为8,仔细观察我们会发现两个结构体的成员变量完全相同只是顺序不同。这也就说明,结构体大小不仅跟成员变量类型有关也跟成员变量的顺序有关。其实,归根结底都是内存对其所导致的问题。
修改默认对齐参数
前面我们说过,不同编译器的对齐参数可能不同,但对齐参数我们可以根据实际需要进行修改(#pragma pack()进行修改,括号内可以是任意想要修改的对齐数的整数值),具体修改如下:
#include
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
4.2 this指针
类中的成员方法中往往是没有用来区分不同对象的成分的,当不同的对象调用该函数时,时如何区分访问的是自己的成员变量成员方法呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性
1. this指针的类型:类类型* const
2. 只能在“成员函数”的内部使用
3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
面试题:this指针存在内存中的那个区域?可以为NULL吗?
1)内存为如下区域:
this指针是类中成员函数的参数,成员函数在调用时会进行压栈,同时会在栈中位参数和局部变量开辟空间将其保存。因此this指针是存在栈中的
注意:this指针是c++中的关键字,同时是const类型,不要受这些因素影响认为this指针存在常量区(代码区)中。
2)this指针可以为NULL,首先this指针是一个指针,指针允许为NULL,只要不对NULL指针进行操作就可以。同时,this指针是C++中隐藏参数,作为类中成员参数,只要函数中不对this指针进行操作就可以为NULL。
// 1.下面程序能编译通过吗?
// 2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
void PrintA()
{
cout<<_a<}
void Show()
{
cout<<"Show()"<}
private:
int _a;
};
int main()
{
Date* p = NULL;
p->PrintA();
p->Show();
}
3)上面代码可以编译通过,编译过程主要任务就是检查语法、生成汇编代码(生成“ . s”文件)。也就是说,我们写的程序如果有语法错误,在编译阶段就会被检查出来,如果不是语法错误编译阶段是检查 不出来的。在上面的代码中,虽然p是一个空指针,但是在类中成员函数是存在公共代码段中的,只有成员变量才是每个对象各自独有,因此Date*的指针即使为NULL也可以访问类中的成员方法(不能访问成员变量)(及时使用类实例化出来的对象不为NULL,给其分配空间也只会保存成员变量),因此上述代码中并没有语法错误。
当编译通过,开始执行程序的时候,p->PtintA()会使程序崩溃,因为PrintA这个成员函数中使用p指针访问了成员变量,而p是一个空指针无法访问成员变量。而p->Show()中没有访问,因此不会出错。