目录
每日鸡汤:
本节重难点总结:
1、C++对成员函数和成员变量分开,让我们只可以调用接口,而不去干涉底层,更加规范。这里还有一个例子可以说明:比如我们要去调栈顶的元素,如果是C++我们直接调用接口即可,可若是C语言,要是有人直接访问成员变量里的size去调呢?而我们不知道这个究竟是size-1指向的数是栈顶还是size,这也会产生冲突。
2、C和C++都可以定义struct--不同的是C++可以在struct中定义函数。
3、C++可以用struct定义类,于class不同的是struct所有内容是public(公开的),class则是分为三个区域,在类外只允许访问public部分。
4、类和类的对象只存储成员变量,类成员函数存储于代码段(常量区),当类中无成员变量时,编译器会给一个字节用于标识。
5、this指针是存在于栈中,是隐式指针形参,不需要用户传递,只能在成员函数内部使用。
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成的。
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现。会发现struct中也可以定义函数。
typedef int DataType; struct Stack { void Init(size_t capacity) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _capacity = capacity; _size = 0; } void Push(const DataType& data) { // 扩容 _array[_size] = data; ++_size; } DataType Top() { return _array[_size - 1]; } void Destroy() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } DataType* _array; size_t _capacity; size_t _size; };相比于上面的结构体,在C++中更喜欢用class来代替。
- class className
- {
- //类体:由成员函数和成员变量组成
-
- };//一定要注意后面的分号
class定义为类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。
类的两种定义方式:
1、声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
//声明和定义全部放在类体中 //人 class Person { public: //显示基本信息 void showInfo() { cout << _name << "-" << _sex << "-" << _age << endl; } public: char* _name;//姓名 char* _sex;//性别 int _age;//年龄 };2、类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
一般情况下,更期望采用第二种方式。这里为了方便演示,大家日后工作尽量使用第二种。
成员变量命名规则的建议:
//我们来看一下这个函数,是不是很难区分?
class Date { public: void Init(int year) { //这里的year到底是成员变量,还是函数形参 year = year; } private: int year; };这样写很难区分谁定义谁。
一般建议都是这样写的:
class Date { public: void Init(int year) { _year = year; } private: int _year; };具体情况具体分析。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
class内部分为三个区域:public(公有) protexted(保护) private(私有);
【访问限定符说明】
1、public修饰的成员在类外可以直接被访问。(通常是一些函数接口)
2、protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。
3、访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止。
4、如果后面没有访问限定符,作用域就到},即类结束。
5、class的默认访问权限为private,struct为public(因为struct兼容了C),也就是class只能在类外访问public的内容,结构体内的变量及成员是可以全部访问的。
所以这里我们也可以来暂时区分一下C++中struct和class:
C++兼容C语言,所以C++中struct可以当作结构体使用。另外C++中结构体还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:这里在继承和模板块,struct和class也有区别。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互。
封装本质上是一种管理,让用户更加方便使用类。
比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB 插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU 、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的, CPU 内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计 算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可 。在C++语言中实现封装,可以 通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
- class Person
- {
- public:
- void PrintPersonInfo();
- private:
- char _name[20];
- char _gender[3];
- int _age;
- };
- // 这里需要指定PrintPersonInfo是属于Person这个类域
- void Person::PrintPersonInfo()
- {
- cout << _name << " "<< _gender << " " << _age << endl;
- }
用类类型创建对象的过程,称为类的实例化。
1、类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储他;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
2、一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量。
int main() { Person._age = 100; // 编译失败:error C2059: 语法错误:“.” return 0; }Person类是没有空间的,只有Person类实例化出的对象才有具体的内容。
3、类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
- class A
- {
- public:
- void PrintA()
- {
- cout << _a << endl;
- }
- private:
- char _a;
- };
- int main()
- {
- A aa1;
- A aa2;
- cout << sizeof(A) << endl;
- cout << sizeof(aa1) << endl;
- cout << sizeof(aa2) << endl;
-
- return 0;
- }
我们来看一下,这个类中既有成员变量,也有成员函数,那么一个类中的对象中包含了什么?是每个类的对象中都有成员变量和成员函数吗?我们如何计算一个类的大小?
说明:类的计算方式是遵循C语言中计算结构体大小内存对齐的原则:
1.第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
3.对齐数==编译器默认的一个对齐数(看编译器)与该成员大小的较小值--vs默认量是8
4.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
5.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍。
按照这个原则的话,按照类中存储成员函数地址(函数指针)算作4个字节,那么这个类的大小是否就是8呢?类对象的大小是否也是8呢?
我们发现,无论是这个类的大小还是类创建的对象的大小都是1,那么类对象的存储方式到底是什么呢?
我们来设想一下,站在设计C++的角度,也就是祖师爷的角度,给出了三种方案:
①对象中包含类的各个成员
我们来看一下这种设计方式,它很明显有很大的缺陷,如果每个对象都像类型一样把类中的成员变量和方法全部存下来,那么也太过浪费空间,比如说:虽然每个对象中成员变量是不同的,但是当调用同一份函数,如果按照此种方式存储,当一个类创建多个对象,每个对象中都会保存一份代码,相同的代码保存多次,浪费空间。显然这种方式比较低效。
②代码只保存一份,在对象中保存存放代码的地址
③只保存成员变量,成员函数存放在公共代码段
第一种方式显然不好,那么第二种和第三种方式哪个好呢?可能有的读者选择第二种,我们让类对象存储函数的地址,把类成员函数存放在一个函数表,当调用的时候我们按照这个地址去寻找,好像可以,也不浪费多大空间。
我们再来看第三种,每个类对象只保存类成员变量,(因为每个类对象所存储的数据有所不同),但是要调用的方法(函数接口)都一样,所以我们把类成员函数放在公共代码区(常量区也叫代码段)。
这两种方式好像都可以,但是第三种要更好,为什么呢?第二种还需要存函数地址,那么每个对象都要存储相同的地址,有点没必要,而第三种直接把函数放在公共区,哪个对象想调用直接可以调用。打个比方再来说明一下:就比如说我们小区的公共健身房,第二种方式就是给小区的每家发一把钥匙,想锻炼了拿钥匙去锻炼,而第三种就是不需要钥匙就可以去健身房锻炼,显然第三种更加方便。
我们C++祖师爷也是这么设计的,这里还有一点问题,看这一段代码:
- class A1
- {
- public:
- void PrintA()
- {
- cout << _a << endl;
- }
- private:
- char _a;
- };
- class A2
- {
- public:
- void f2()
- {}
- };
- class A3
- {};
- int main()
- {
- cout << sizeof(A1) << endl;
- cout << sizeof(A2) << endl;
- cout << sizeof(A3) << endl;
- return 0;
- }
我们来看这样一段代码,A1很好理解,A2和A3的大小为什么是1呢?A2里面只有成员函数,它们不是被放在代码段吗?为什么会有大小呢?还有A3,没有成员,为什么大小也是1呢?
我们可以反过来想想,如果A2和A3它们大小都是0,不占空间,那么我们编译器怎么区分它们呢?我们祖师爷也想到了这一点,所以:
一般一个有成员变量的类的大小,实际就是该类中各“成员变量”之和,当然要注意内存对齐。 对于没有成员变量的空类(A3)和没有成员变量只有成员函数的类(A2),编译器给了一个字节来唯一标识这个类的对象。
我们先来定义一个日期类Date
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- void Print()
- {
- cout <<_year<< "-" <<_month << "-"<< _day <
- }
- private:
- int _year; // 年
- int _month; // 月
- int _day; // 日
- };
- int main()
- {
- Date d1, d2;
- d1.Init(2022,1,11);
- d2.Init(2022, 1, 12);
- d1.Print();
- d2.Print();
- return 0;
- }
可能大家看这段代码不知所云博主想表达什么,我想问一下,这里的d1.Print()和d2.Print()打印出来的相同吗?可能读者下意识觉得肯定不相同啊,类对象都不相同,但是博主上面说过类函数成员存在于代码段,也就是类对象调用函数时都是从代码段调用,那么它们既然调用相同的函数,还没有传参,为什么会不相同呢?
我们可以看到它们打印的并不一样,说明我们的C++设计时考虑到了这个问题,所以这里就要引入this指针。
C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过调用该指针去访问。只不过所有的操作对用户是隐形的,用户不需要来传递,编译器自动完成。
8.2 this指针的特性
1、this指针的类型:类类型*const,即成员函数中,不能给this指针赋值。
2、只能在"成员函数"的内部使用
3、this指针本质上是“成员函数”的形参,在建立函数栈帧时传递,所以this存在于栈中。当对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
4、this指针是“成员函数”第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器(此处是vs编译器)自动传递,不需要用户传递。
我们是可以在类里面用this指针的。
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- void Print()
- {
- cout << this << endl;
- cout << this->_year <<" "<< this->_month<<" "<< this->_day << endl;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
- int main()
- {
- Date d1, d2;
- d1.Init(2022, 9, 22);
- d2.Init(2022, 9, 23);
- d1.Print();
- d2.Print();
- return 0;
- }
经过上述的讲解,再来看一下两道面试题:
不知道各位老铁的答案是什么,反正这道题目有点苟,博主第一次碰到也做错了。我猜一下大家会选什么:大家可能一致选B、运行崩溃。哈哈,博主刚开始也是这么选的,答案是第一个选C正常运行和第二个B、运行崩溃。
第一个为什么可以正常运行呢?我们来分析一下,p是空指针,p解引用调用函数Print,但是这里有个点博主上面说过,这里并没有对p进行解引用,因为类成员函数存在于代码段,所以而且成员函数里也没有对p的地址解引用,所以正常运行,不会出现解引用空指针导致运行崩溃。
第二题,不一样的是,这里有个隐藏this->_a,this是p空指针,对空指针解引用找成员变量会出现运行崩溃。
8.3 C语言和C++实现Stack的对比
博主也是纠结了一下要不要放栈的代码,因为感觉有点水文,最后还是放在了这里,不想看的老铁可以收起代码。
①C语言实现
- typedef int STDataType;
-
- typedef struct Stack
- {
- STDataType* a;
- int top;
- int capacity;
- }ST;
- void StackInit(ST* ps);
- void StackDestory(ST* ps);
- void StackPush(ST* ps,STDataType x);
- void StackPop(ST* ps);
- STDataType StackTop(ST* ps);
- bool StackEmpty(ST* ps);
- int StackSize(ST* ps);
-
-
- void StackInit(ST* ps)
- {
- assert(ps);
- ps->a = NULL;
- ps->top = ps->capacity = 0;
- }
- void StackDestory(ST* ps)
- {
- assert(ps);
- free(ps->a);
- ps->a = NULL;
- ps->capacity = ps->top = 0;
- }
- void StackPush(ST* ps, STDataType x)
- {
- assert(ps);
- //只能尾插
- if (ps->top==ps->capacity)
- {
- int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
- STDataType* tmp = (STDataType*)realloc(ps->a, newCapacity * sizeof(STDataType));
- if (tmp == NULL)
- {
- perror("realloc fail");
- exit(-1);
- }
- ps->a = tmp;
- ps->capacity = newCapacity;
- }
- ps->a[ps->top] = x;
- ps->top++;
- }
- void StackPop(ST* ps)
- {
- assert(ps);
- assert(!StackEmpty(ps));
- --ps->top;
- }
- STDataType StackTop(ST* ps)
- {
- assert(ps);
- assert(!StackEmpty(ps));
- return ps->a[ps->top - 1];
- }
- bool StackEmpty(ST* ps)
- {
- assert(ps);
- return ps->top == 0;
- }
- int StackSize(ST* ps)
- {
- assert(ps);
- return ps->top;
- }
可以看到,在用C语言实现时,Stack相关操作有以下共性:
1、每个函数必须传结构体变量的地址
2、函数中都对第一个参数检测,因为该参数可能为NULL
3、函数都是通过Stack*参数操作栈的。
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,设计到大量指针操作,稍不注意就会出错。
②C++实现
- typedef int DataType;
- class Stack
- {
- public:
- void Init()
- {
- _array = (DataType*)malloc(sizeof(DataType) * 3);
- if (NULL == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
- _capacity = 3;
- _size = 0;
- }
- void Push(DataType data)
- {
- CheckCapacity();
- _array[_size] = data;
- _size++;
- }
- void Pop()
- {
- if (Empty())
- return;
- _size--;
- }
- DataType Top()
- {
- return _array[_size - 1];
- }
- int Empty()
- {
- return 0 == _size;
- }
- int Size()
- {
- return _size;
- }
- void Destroy()
- {
- if (_array)
- {
- free(_array);
- _array = NULL;
- _capacity = 0;
- _size = 0;
- }
- }
- private:
- void CheckCapacity()
- {
- if (_size == _capacity)
- {
- int newcapacity = _capacity * 2;
- DataType* temp = (DataType*)realloc(_array, newcapacity *
- sizeof(DataType));
- if (temp == NULL)
- {
- perror("realloc申请空间失败!!!");
- return;
- }
- _array = temp;
- _capacity = newcapacity;
- }
- }
- private:
- DataType* _array;
- int _capacity;
- int _size;
- };
C++中通过类可以将数据以及操作数据的方法进行完美结合,通过访问权限可以控制哪些方法在类外可以被调用,即封装。也就是C++更加规范。总之C++中Stack* 参数是编译器自己维护的,C语言中需要用户自己维护。