C++是一门基于对象的语言,不是完全面向对象的语言(Java时一门纯面向对象的语言)。那么什么事面向对象呢?面向对象就是我们关注的时对象,我们把一件事情分成几部分来协同完成一件事,每一部分我们称为对象。C语言是一门面向过程的一门语言,面向过程就是我们把一件事情按步骤完成,每一步都是通过函数调用完成。
在C语言中我们在结构体中只能定义数据成员,不可以定义函数。C++中对结构体进行了扩展,既可以定义成员函数(结构体中定义的函数),也可以定义数据成员(结构体中定义的数据),只不过在C++中给这种结构体中定义函数另外起来一个名字就是类。把struct关键字换为class替代。
一、类
class为定义类的关键字。如下一个例子:
class A1{
void fun(){}
int a;
}; //注意此处分号
类中的元素称为类的成员,类中的数据称为类的属性或者成员变量,类中的函数称为类的方法或者成员函数。
二、类的两种定义方式
1. 把成员函数声明和定义全放在类中
这种情况下编译器可能吧成员函数认为是内联函数处理。
class Student{
void set_student(char *name, int age, char *gender)
{
strcpy(_name, name);
_age = age;
strcpy(_gender, gender);
}
void print_student();
char _name[20];
int _age;
char _gender[4];
};
2. 吧成员函数定义在类外面
这种情况需要在函数名前面加一个作用域限定符 ::,不加的话,编译器吧他当成为普通的全局函数。
void Student::set_student(char *name, int age, char *gender)
{
strcpy(_name, name);
_age = age;
strcpy(_gender, gender);
}
三、类的访问限定符
1. public修饰的成员可以再类外被直接访问。
2. protected和private可以通过友元函数访问。
3. 访问权限的作用域时从访问限定符开始直到出现下一个访问限定符出现。
4. 如果不写访问权限,,class默认是private,而上述我们所说的struct定义的类是public(因为C++要兼容C语言结构体,如果丁定义成private,那么我们定义的结构体我们都访问不到成员了)。
注:访问限定符只在编译期间起作用,当数据映射到内存之后,没有访问限定符区别。
问题一:C++中class和struct有什么区别?
C++需要兼容C语言结构体,所以struct可以定义结构体。除此之外struct还可以定义类和class一样,区别去要在于,struct定义的类默认访问权限是public,class定义的类默认访问权限时private。
问题二:C++中struct和C语言struct区别?
C++中struct可以定义结构体,也可以定义类(主要体现在成员可以是函数)。C语言中struct只可以定义结构体。
四、C++三大特性之一——封装
面向对象的三大特性:封装、继承、多态。
封装:用类将对象的属性和方法放到一块,也就是隐藏对象的属性和实现细节。通过访问限定符选择性的将类的对外接口供用户使用。封装本质上就是一种管理。就比如我们如何管理兵马俑。当我们发现兵马俑,先给他建一顶房子,把它全封闭,我们封装起来就是不让别人看,但是我们会通过卖门票的方式供游客参观。类也是一样,吧类的数据和方法封装在一起,通过访问限定符来确定用户是否可以看到。所以封装本质上就是管理。
五、类的作用域
定义类同时定义了一个类作用域。在类外定义成员函数时需要使用作用域限定符来指明。
类中的成员变量在类中具有全局属性,同时与定义位置没有关系。(后面讲为什么?)
C++作用域:1.命名空间 2. 类 3. 全局作用域 4. 局部作用域
六、类的实例化
创建对象的过程就是类的实例化。类就是一种类型,我们可以把他想成int类型,他不占用内存大小,但是可以定义很多变量,变量占用内存空间,这里的变量就相当于类定义的对象(当然内存空间只为对象的成员变量开辟空间,而成员函数没有开辟在一起)。
1. 定义一个类并没有为之开辟空间来存储它。
2. 一个类可以实例化很多个对象,对象占用实际存储空间,存储类成员变量。
七、类对象的存储
前面我们已经说过类不占内存空间,但是为对象开辟内存空间,但是仅仅是为了存储成员变量,难道不给成员函数函数开辟内存吗?不是的,因为我们用同一个类实例化多个对象,但是每个对象的成员变量值后续操作可能不同,但是它们调用函数调用的都是一模一样,所以如果每实例化一个对象都给它开辟空间来存储同一份代码岂不是太浪费空间,所以我们吧成员函数另外开辟到了其他地方。
1)计算类的大小,先看几个特例
class A1{
void fun(){}
int a;
};
class A2{
void fun(){}
};
class A3{
};
上述类中A1:4B A2:1B A3:1B
结论:一个类的大小实际就是类中数据成员的大小之和,当然需要考虑内存对齐(和结构体一样);特例是当一个类没有成员或者没有成员变量,一般编译器都会给1个字节。(并不是所有的编译器默认都给1个字节)
结构体内存对齐回顾:
(1)第一个成员在与结构体偏移量为0的位置。
(2)其他成员变量需要对齐到对齐数的整数倍(对齐数 = 编译器默认对齐数和成员变量类型大小的较小值)。Vs默认为8,gcc默认4,通过#pragma pack()调整。括号内什么也不写表示默认,1不对齐,2,4,8,16.一般对齐数只能往小了调整
(3)结构体的总大小是最大对齐数的整数倍。
(4)结构体中包含结构体(嵌套的结构体对齐到自己的最大对齐数的整数倍)。结构体的整体大小就是所有最大对齐数(包含嵌套的结构体的对齐数)的整数倍。
- 为什么要内存对齐
i)平台原因(移植原因)
不是所有的硬件设备可以访问任意地址上的任意数据,某些硬件平台只能在特定地址上访问特定数据类型。否则刨出异常
ii)性能原因
原因在于,为了访问未对齐的内存数据,cpu需要访问两次内存,而对其的内存只需要访问一次就可以。
所以,我们在设计结构体的时候我们既要内存对齐,又需要节省空间,我们可以让占用空间小的成员尽量集中到一起。
问题一:如何知道结构体中某个成员的偏移量?
通过宏offsetof(s,m)可以知道。
Student s1;
cout << offsetof(Student, _gender) << endl;问题二:offsetof宏是怎么实现的?原型:#define offsetof(s,m) (size_t)&(((s *)0)->m)
struct type{ int a; int b; int member; int c : 1; };
其实offsetof就是一个宏定义,我们也可以自己实现的。接下来就来分析这个宏的具体实现。
#define offsetof(type, member) (size_t)&(((type *)0)->member)
type是一个结构体,定义如上所示,它有一个member成员,返回的是结构体type的成员member相对type的偏移地址。
这里的一个妙处就:(type *)0骗编译器说有一个指向结构体type的指针,地址值是0。
然后在取该指针的member地址 &((type *)0)->member,因为基址是0,所以这时member的地址就是member在type中的偏移量了。
最后转换成size_t型,即unsigned int。
八、this指针(this是一个关键字)
先看问题:当我们定义一个类,然后实例化多个对象S1,S2.在成员函数中没有对不同对象进行区分,为什么我们调用S1的成员函数,编译器怎么知道我们要调用S1的成员函数,而不是S2对象的?
这就是因为this指针。C++中给每一个成员函数增加了一个隐藏的指针参数(第一个参数位置),该指针指向当前对象,当调用函数时,调用该对象的函数,在函数中所有成员变量的操作,全是this指针操作的。只不过这一切都是编译器为咋们做的,当然我们也可以把this指针写出来。如下:
//下面两种成员函数一样
void SetData(int year,int month,int day)
{
_year=year;
_month=month;
_day=day;
}
void SetDate(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
//对应调用方式
s1.SetDate(1, 2, 3);
Date::SetDate(&s1, 1, 2, 3);
上述其实就是对成员函数的改造。
编译器在编译期间对类主要做三件事:(按顺序)
(1)识别类名。
(2)识别类中的成员变量。
(3)识别类中的成员函数 & 改造成员函数
前面我们提到一个问题就是为什么类成员变量在类中具有全局属性而且与他定义位置无关。就是因为编译器在编译期间对类的操作步骤是先识别类中成员变量,然后识别成员函数。所有说,成员变量在成员函数后面也不影响成员函数中成员变量的使用。
this指针的特性:
(1)this指针的类型:类名*const this;
(2)this指针只能在成员函数(非静态成员函数)内部使用,其他地方不可以使用;
(3)this指针其实就是成员函数的一个参数,当调用对象的成员函数时,吧对象的地址传给参数this指针。所以对象的大小中不包含this指针。
(4)this指针是成员函数隐藏的参数,是第一个参数位置。一般情况this指针由ecx寄存器自动传递,不需要我们来做这件事。
并不是所有的this指针都是通过ecx寄存器传递,这是由调用约定决定的。
C++常见两种调用约定:
(1)_cdecl:参数调用由压栈来传递,从右向左传递。可变参数函数,只能通过这种约定传递。
(2)_thiscall:这种调用方式,主要用于成员函数。在成员函数中,除了this指针是由ecx寄存器传递,其他参数都是从右向左压栈传递。
问题一:this指针可以为空吗?
可以,下面一段程序:
Student s1;
Student *ps = &s1;
ps->SetStudent();//程序不会挂掉,因为这一步其实是Student :: SetStudent(ps),并没有发生解引用,但是this指针确实空的,所以this可以为空。但是这个程序最终会不会挂掉还要看函数里面有没有对this指针解引用。