初步认识面向过程与对象
- 在之前初步学习C语言后,可以了解到C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
- C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
类的引入
C语言中结构体只能定义变量,而在C++中,结构体内不仅可以定义变量,也可以定义函数。
struct Stack { //成员变量 int* _arr; int _size; int _capacity; //成员函数 //初始化 void Init(int DefaultCapacity = 4) { _arr = (int*)malloc(sizeof(int) * DefaultCapacity); if (_arr == nullptr) { perror("malloc fail"); return; } _size = 0; _capacity = DefaultCapacity; } //销毁 void Destory() { if (_arr) { free(_arr); _arr = nullptr; _size = 0; _capacity = 0; } } //入栈 void Push(int x) { if (_size == _capacity) { //扩容 int* arr = (int*)malloc(sizeof(int) * _capacity * 2); if (arr == nullptr) { perror("malloc fail"); return; } _arr = arr; arr = nullptr; } _arr[_size] = x; _size++; } //出栈 void Pop() { _size--; } //求栈顶元素 int Top() { return _arr[_size - 1]; } };
由于C++兼容C语言,struct在C语言阶段的语法都可以使用,但在C++中又对struct进行了优化,将struct升级成了类。
int main(void) { //C语言中struct的用法 struct Stack st1; //C++中strcut的用法 Stack st2; }
类的定义
虽然C++中可以使用struct,但是C++更喜欢使用class代替struct。
class classname { //类体:由成员函数和成员变量组成 };//注意分号
class为定义类的关键字,classname为类的名字,{ }中为类的主体,注意类定义结束时返回后面分号不可省略。
类体中内容被称为类的成员;类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。
- 类定义的俩种方式:
1.声明和定义全部放在类中,这种方式会默认就是内联函数(inline)
class Stack { //成员变量 int* _arr; int _size; int _capacity; //成员函数 //初始化 void Init(int DefaultCapacity = 4) { _arr = (int*)malloc(sizeof(int) * DefaultCapacity); if (_arr == nullptr) { perror("malloc fail"); return; } _size = 0; _capacity = DefaultCapacity; } //销毁 void Destory() { if (_arr) { free(_arr); _arr = nullptr; _size = 0; _capacity = 0; } } //入栈 void Push(int x) { if (_size == _capacity) { //扩容 int* arr = (int*)malloc(sizeof(int) * _capacity * 2); if (arr == nullptr) { perror("malloc fail"); return; } _arr = arr; arr = nullptr; } _arr[_size] = x; _size++; } //出栈 void Pop() { _size--; } //求栈顶元素 int Top() { return _arr[_size - 1]; } };
2.类声明放在.h文件中,成员函数放在.cpp文件中
【注意】在.cpp文件中函数需要加类名::
//.h文件 class Stack { //成员变量 int* _arr; int _size; int _capacity; //成员函数 //初始化 void Init(int DefaultCapacity = 4); //销毁 void Destory(); //入栈 void Push(int x); //出栈 void Pop(); //求栈顶元素 int Top(); };
//.cpp文件 //Stack初始化 void Stack::Init(int DefaultCapacity) { _arr = (int*)malloc(sizeof(int) * DefaultCapacity); if (_arr == nullptr) { perror("malloc fail"); return; } _size = 0; _capacity = DefaultCapacity; } //stack销毁 void Stack::Destory() { if (_arr) { free(_arr); _arr = nullptr; _size = 0; _capacity = 0; } } //Stack入栈 void Stack::Push(int x) { if (_size == _capacity) { //扩容 int* arr = (int*)malloc(sizeof(int) * _capacity * 2); if (arr == nullptr) { perror("malloc fail"); return; } _arr = arr; arr = nullptr; } _arr[_size] = x; _size++; } //Stack出栈 void Stack::Pop() { _size--; } //求栈顶元素 int Stack::Top() { return _arr[_size - 1]; }
成员变量命名规则的建议:
class Date { void Init(int year) { year = year; } int year; };
观察这段代码,首先这段代码是正确的,但是由于局部域与类域中的变量相同导致无法理解=左边和=右边的year是函数参数,还是成员变量。
首先,需要清楚的是,编译器会先搜索局部域,然后再去搜索类域。赋值是将右面的值赋给左边,所有右边的year是局部的函数参数,而左边的year是类域中的值。但是这种情况会很僵硬,所有推荐以下几种写法:
- 在成员变量前加_
class Date { int _year; };
- 或者在成员变量后加_
class Date { int year_; };
- 或者在成员变量前加m
class Date { int myear; };
类的封装
面向对象的三大常见特征:封装、继承、多态
何为封装?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
- 封装本质上是一种管理,让用户更方便使用
在C++中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪种方法可以在类外部直接使用。
类的访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一起,让对象更加完整,通过访问权限选择性的将其接口提供给外部的用户使用。
class Stack { public: //成员函数 //初始化 void Init(int DefaultCapacity = 4); //销毁 void Destory(); //入栈 void Push(int x); //出栈 void Pop(); //求栈顶元素 int Top(); private: //成员变量 int* _arr; int _size; int _capacity; };
- 访问限定符说明:
1.public修饰的成员在类外可以直接被访问
2.protected和private修饰的成员在类外不能直接被访问(在前期学习的阶段protected和private是类似的,在继承中有差异)
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。(从上面的代码:public——>private,之间为公有)
4.如果后面没有访问限定符,作用域就到 } 即类结束(从上面的代码:private——>},之间为私有)
5.class的默认访问权限为private,struct为public(因为struct兼容C语言)
【注意】访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符的区别。 - C++中struct和class的区别
C++中需要兼容C语言,所有C++中struct可以当成结构体使用,另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private,后续在继承和模板参数列表位置,struct和class也有区别。
类的作用域
现阶段所了解到域:全局域、局部域、命名空间域、类域。
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用::作用域操作符指明成员属于那个类域。
//Stack初始化 void Stack::Init(int DefaultCapacity) { _arr = (int*)malloc(sizeof(int) * DefaultCapacity); if (_arr == nullptr) { perror("malloc fail"); return; } _size = 0; _capacity = DefaultCapacity; } //stack销毁 void Stack::Destory() { if (_arr) { free(_arr); _arr = nullptr; _size = 0; _capacity = 0; } } //Stack入栈 void Stack::Push(int x) { if (_size == _capacity) { //扩容 int* arr = (int*)malloc(sizeof(int) * _capacity * 2); if (arr == nullptr) { perror("malloc fail"); return; } _arr = arr; arr = nullptr; } _arr[_size] = x; _size++; } //Stack出栈 void Stack::Pop() { _size--; } //求栈顶元素 int Stack::Top() { return _arr[_size - 1]; }
编译器在搜索变量时,会先在局部域搜索,然后再去类域中搜索,最后去访问全局域,由于命名空间域与全局域几乎是平行的,如果命名空间域不展开、不指定,编译器是不会去访问的,局部域与全局域会影响声明周期的,而类域与命名空间域是不会影响声明周期的。
类的实例化
使用类类型创建对象的过程,被称为类的实例化。
类是对对象进行描述的,像模型一样,限定了类的成员,定义出一个类并没有分配实际的内存空间存储。
类中的成员函数的声明与定义可以观察其是否在类中进行实现,而类中的成员变量是声明,成员变量的声明与定义的判定是观察其是否开空间,而成员变量的定义一般是整体定义,这个整体定义成员变量叫做类实例化对象或者对象定义。
一个类可以实例化多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量。
类的大小
类的大小只计算成员对象,不计算成员函数
class Date { public: void Init(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main(void) { cout << sizeof(Date) << endl; return 0; }
C++与C语言一样,都存在内存对齐。
class A { public: void Func() { cout << "Func()" << endl; } private: int i; char c; }; int main(void) { cout << sizeof(A) << endl; return 0; }
由于每个对象中的成员变量是不同的,每个变量都应该拥有自己的存储空间;但是函数不同,每一次都调用的是同一份函数,如果按照这种方式存储,当一个类创建多个对象是,每一个对象都会保存一份相同的函数代码,浪费空间。所以类的大小只会计算成员对象,不计算成员函数。
可以举个例子供大家理解:
成员函数相当于小区的公园、健身房、篮球场一样这些公共的场所;
成员函数相当于小区里每个房子的卧室、厨房、卫生间这些独有的变量;
当你调用这个类创建对象时,你就相当于在这个小区拥有了住所,有居住的权限,同时也可以享受小区里公共场所的服务;
计算类的大小,相当于计算每一个房子的面积。
class A { public: void Func() { cout << "Func()" << endl; } int _i; char _c; }; int main(void) { A a1; a1._i = 1; a1.Func(); return 0; }
从反汇编的角度可以察觉到当对象引用成员变量时,会有自己的存储空间;而调用函数时,会call函数的地址去寻找。
//类中即有成员函数,又有成员变量 class A { void Func() { cout << "Func()" << endl; } int _i; char _c; double _d; }; //类中只有成员函数 class B { void Func() { cout << "Func" << endl; } }; //类中什么都没有 class C { }; int main(void) { cout << sizeof(A) << endl; cout << sizeof(B) << endl; cout << sizeof(C) << endl; return 0; }
观察这三种情况:
1.类中既有成员函数、也有成员变量
计算成员变量的大小。
2.类中只有成员函数
没有成员变量的类对象,需要1byte,是为了占位,表示对象存在,不存储有效数据。
3.类中什么都没有
空类比较特殊,编译器给了空类一个字节来唯一辨识这个类的对象。
- 结构体内存对齐规则
1.结构体的第一个成员,对齐到结构体在内存中存放位置的0偏移处
2.从第二个成员开始,每个成员都要对齐到(一个对齐数)的整数倍处
对齐数:
结构体成员自身大小和默认对齐数的较小值
在VS中:默认对齐数为8
Linux gcc:没有对齐数,对齐数就是成员自身大小
3.结构总大小为最大对齐数的较小值
4.如果结构体中嵌套了结构体成员,要将嵌套的成员对齐到自己的成员中最大对齐数的整数倍处
5.结构体的总大小必须是最大对齐数的整数倍,这里的最大对齐数是:包含嵌套结构体成员中的对齐数的所以对齐数中的最大值
- 结构体内存对齐的原因:
1.平台原因:
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在地址处取某些特定类型的数据,否则会抛出硬件异常
2.性能原因:
数据结构(尤其是栈)应该尽可能的在自然边界上对齐,原因在于,为了访问来对齐的内存,处理器需要作俩次内存访问,而对齐的内存仅需要一次访问
总结:
结构体的内存对齐是拿空间来换取时间的做法(满足对齐,节省空间:让占用空间小的成员尽量集中在一起)
this指针
class Data { public: void Print() { cout << _year << "/" << _month << "/" << _day << endl; } void Init(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main(void) { Data d1; Data d2; d1.Print(); d2.Print(); d1.Init(2004,10,10); d2.Init(); return 0; }
观察这段代码,Date类中有Init和Print俩个成员函数,但是函数体中没有关于调用某个对象的区分,也就是说,类中成员函数是如何区分对象进行操作的。
在C++中引入this指针解决该问题:
即C++编译器给每个“非静态的成员函数”增加一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针访问。
只不过所有的操作对用户都是透明的,即用户不需要传递,编译器会自动完成。
void Print(Data* const this) { cout << this->_year << "/" << this->_month << "/" << this->_day << endl; }
所以本质上,函数在调用时都会传递一个指针参数,this代替了对象的指针,即调用的虽然时同一个指针,但是形参不同
但是上面这段代码是错误的,因为this指针不在形参和实参显示传递,但是可以在函数内部显示使用。
所有正确的代码是:
void Print() { cout << this->_year << "/" << this->_month << "/" << this->_day << endl; }
this指针的特性
- this指针的类型:类类型*const,即成员函数中,不能给this指针赋值。
- 只能在成员函数内部使用。
- this指针本质上是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给this指针,this指针跟普通参数一样存储在函数调用的栈帧里,作为栈帧的一部分,对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
观察俩段经典的代码:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
这段代码的结果是正常运行,虽然p是nullptr,但是在调用Print成员函数的时候,没有发生解引用操作,因为Print成员函数的地址不在对象中,p会作为实参传递给this指针,this指针也为nullptr,但是函数内部并没有对this指针进行解引用。
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }
this指针是空的,但是函数内部访问了_a,本质上是this_a,所以会运行崩溃。