目录
1.面向过程和面向对象初步认识
2.类的引入
3.类的定义
3.1类的两种定义方式:
3.2 成员变量命名规则建议
4.类的访问限定符及封装
4.1 访问限定符
4.2.封装
5.类的作用域
6.类的实例化
7.类对象模型
7.1 如何计算类对象的大小
7.2 类对象的存储方式猜测
7.3 结构体内存对齐规则
8.this指针
8.1 this指针的特性
8.2 this指针相关面试题
1. this指针存在哪里?
2. this指针可以为空吗?
8.3. C语言和C++实现Stack的对比
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成
我们现在进入c++的学习之后,我们就不用关注做事情的过程,而是关注做事情对应的对象了。比如洗衣服,我们出现了四个对象—(人,衣服,洗衣粉,洗衣机),但是过程中对衣服的繁琐过程比如(打开洗衣机,倒入洗衣机...等)是不需要关心的。
为了更加的对面向对象的c++语言有更深的认识,继续往下看吧。
- C语言结构体中只能定义变量
图一:c语言——对于这个结构体来说:
struct ListNode
是这个结构体的类型,struct必须带上的。图二:c++语言——c++将结构体定义成了类,C++中可以直接用struct后面的做结构体类型,可以不用加struct。
- 在C++中,结构体内不仅可以定义变量,也可以定义函数。
比如:
之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数。
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; } 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是类的关键字,用class来表示类。
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量;
类中的函数称为类的方法或者成员函数。
1. 声明和定义全部放在类体中(下面的成员变量其实是声明)
需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
那在成员函数showlnfo前面加上Person::的作用呢其实就是告诉编译器showlnfo不是全局的函数,而是Person这个类中的成员函数,如果不加就会报错的,因为在全局找不到该函数。那这样如果在函数中用到了对应的成员变量,编译器也会到类中去寻找。另外要注意如果有缺省参数前面我们说了要在函数声明中给。
class Date
{
public:
void Init(int year)
{
//这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
//成员变量
int year;
int month;
int day;
};
Date有一个成员变量(属性)year,然后还有一个成员函数Init,但是Init函数的形参和成员变量同名,那这里就有一个问题,Init中的year到底是成员变量,还是函数形参?
class Date
{
public:
void Init(int year)
{
//这里的year到底是成员变量,还是函数形参?
_year = year;
}
private:
//成员变量
int _year;
int month;
int day;
};
成员变量的前面我们可以加一个_和形参进行区分。
这只是建议,大家可以按照自己的想法进行区分。以后大家进入公司工作主要看公司要求。
C++实现封装的方式:用类将对象的属性(成员变量)与方法(成员函数)结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
【访问限定符说明】
- 1. public修饰的成员在类外可以直接被访问
- 2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
现阶段我们刚开始学习类和对象,可以先不在意protected和private具体的区别。
- 3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 4. 如果后面没有访问限定符,作用域就到 } 即类结束。
- 5. class的默认访问权限为private,struct为public(因为struct要兼容C)
我们在这里没有设定访问限定符,这里说明了class默认访问权限是private,所以是不供用户进行访问的。
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家介绍。
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。
比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
这个大家先了解一下即可,在后续学习过程中我们还会不断加深对封装等特性的理解。
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
//在类里面定义函数
public:
void PrintPersonInfo(const char*_name,const char*_gender,int _age);
//在类里面定义变量
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo(const char*_name, const char*_gender, int _age)
{
cout << _name << " " << _gender << " " << _age << endl;
}
int main()
{
Person p;
p.PrintPersonInfo("peter", "男", 19);
return 0;
}
这里的PrintPersonInfo是属于Person这个类域,格式Person(类名)::PrintPersonInfo(成员函数)。
来表明PrintPersonInfo成员函数是属于Person的。
用类类型创建对象的过程,称为类的实例化
1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
类就好比是建造房子的图纸,类的实例化就好比用图纸去建造房子。图纸只是对房子进行了一个描述,用图纸建造出来的房子(类的实例化)才占用实际空间。
类只是一个图纸,不占据空间,类的实例化就是对图纸的内容进行创建一个实打实的物体并且占据空间。
举个例子吧:
类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。
谜语:"年纪不大,胡子一把,主人来了,就喊妈妈" 谜底:山羊
2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄
man就是我们person这个类实例化出来的一个对象。
3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间,
class A
{
public:
void PrintA()
{
cout << _a << endl;
}
private:
char _a;
};
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
这里的A和aa1的大小都是1,不考虑成员函数。
我们猜测,有以下几种可能:
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
对于上述三种存储方式,那计算机到底是按照那种方式来存储的,下面我们就来验证一下:
我们再通过对下面的不同对象分别获取大小来分析看下:
首先提醒大家C++中类对象大小的计算方法和C语言结构体是一样的,都要考虑内存对齐。
首先给类Date创建一个对象的d1,看它的大小是多大?
我们只看类中的成员变量。大小是12.
所以呢:
没错,正确的存储方式是第三种:类对象中只存储成员变量,不存储成员函数(地址也没有),成员函数存放在公共的代码段。
打个比方,大家可以这样理解:
我们说了类就好比是建造房子的图纸,一张图纸可以建造多个房子,那同样道理,一个类就可以实例化多个对象。
- 类中对象的属性(成员变量)呢?
就可以看作是房子里面的厨房、浴室…这些东西,每栋房子里面都有。
- 类中的成员函数(方法)呢?
就可以看作小区里的篮球场,小卖部等,这种东西需要一个房子里面建一个吗?那就太浪费了吧,是不是整个小区共用一个就行了啊。所以成员函数是不存在对象里的,而是存在公共的代码段。
总结:
计算一个类对象的大小,只需要考虑其中的成员变量就行了,当然记得要按照结构体内存对齐的规则进行计算。
下面我们就来做几个练习,计算几个类的大小
三种情况的题目:
类的大小是多大,它创建的对象就是多大,就像整型int的大小是4个字节,用int创建的变量也是4个字节。
class A1 {
public:
void f1(){}
private:
int _a;
};
class A2 {
public:
void f2() {}
};
没有成员变量,只有成员函数,那么它的大小是0吗?
// 类中什么都没有---空类
class A3
{};
一个空类,那按照上面的结果来分析,A3的大小也应该是0,也是一个成员变量都没有:
是的,也是1.
那为什么不含成员变量的类大小是1个字节呢?
- 一个类的大小,实际就是该类中”成员变量”大小之和,当然要注意内存对齐。
仅有成员函数,没有成员变量,编译器都给了一个字节来唯一标识这个类的对象。
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
- 空类有意义嘛?——相信任何东西存在就有意义,只是我们现在还不知道。
——我们可以这样,如果空类或者仅有成员函数,没有成员变量,怎么表示它定义了,这就需要编译器默认给它们的大小为1.证明它们虽然没有成员变量,但是定义了。
- 1. 第一个成员在与结构体偏移量为0的地址处。
- 2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8
- 3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
我们在C语言阶段也详细讲解过了,大家不熟悉的可以复习一下:
链接: 结构体内存对齐
C语言专栏中的一篇文章:自定义类型(结构体,枚举,联合,位段)
为什么要内存对齐?——读取的时候只能在对齐数的倍数上起始位置上读
#include
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
int a;
};
那我们现在用该类创建两个对象,并调用成员函数:
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
我们先来定义一个日期类Date。
我们看到这里能够正确的对d1,d2的属性(成员变量)进行初始化并打印。
我们运行这段代码,查看了结果,为什么d1,d2调用的是同一个函数,调同一个函数,为什么打印的结果是不一样的呢?
答:——我们大多数是第一想法就是对象不一样,Date类有俩个对象d1,d2,对象的不同调用同一个函数的结果就不同。
可是调用函数和对象有关系嘛?
对于上述类,有这样的一个问题:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
那原因在于:
C++中通过引入this指针解决该问题,
即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
以前对于c中的栈什么的,都是自己手动写出来代码,而在c++中编译器不会显示给用户,自动给成员函数传地址。——比如this指向的是d1的年月日,那么就会打印对象d1的内容。
1. this指针的类型:类型* const,即成员函数中,不能给this指针赋值。
比如在上面的Date类中,this指针的类型就是
Date* const
我们这来复习一下const* / *const 的区别——c语言回顾
const:修饰离const最近的第一个成形类型。
区分:const int* p2=&a ; int const*p3=&a 这俩者的修饰的类型和修饰的内容都是一样的,修饰的内容是无法修改的(简单记成const在*前面修饰的类型(int,float,double),而不是(int*,float*,double*...)
区分:int *const p4=&b; const在*之后修饰的类型(int*,float*,double*),,是不可以修改constt之后的内容。
- *在const之前,修饰的类型一定是 int*类型。 int *const p4;表示修饰的内容是p4,修饰的类型是int。 p4是不可以修改的。
- *在const之后,修饰的类型一定是 int类型. int const *p4//const int *p4;表示修饰的内容是*p4,修饰的类型是int. *p4是不可以修改的。修饰的内容是无法改变的
我们复习了const的内容,我们知道const修饰的内容是不可以修改的,所以是在后面Date *const this ,而不能 const Date *this或者Date const *this ,这样修饰的内容是*this ,这里就表示*this是不可以修改的。
左操作数必须为左值指一个能用于赋值运算左边的表达式。左值必须能够被修改,不能是常量权。我们用变量作左值,还可以看到,指针和引用也可以作左值。
一般形式为:变量=表达式。其作用是将一个表达式的值赋给一个左值。计算赋值运算符右侧表达式的值(“=”为赋值运算符),将赋值运算符右侧表达式的值赋给左侧的变量,将赋值运算符左侧的变量的值作为表达式的值。
所以可以表示this指针是不可以修改的。
2. 只能在“成员函数”的内部使用
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
有些易错点:
我们将成员变量设置成公共部分,能不能通过类直接访问呢?——不能
为什么不能?成员变量只是声明,就好比在图纸里面找到自己的房间,图纸里面有住的房间嘛?抱歉没有你的房间,还有人这样写
::这是域限定符号,怎么能通过对象访问成员变量呢?
.符号,.就相当于对象访问成员的一种符号。
我们的步骤必须,给类创建对象,然后通过对象访问成员函数之间的符号是(.),而不是::域限定符号。
不能通过类访问成员变量和成员函数,所以不能在图纸中找到自己房间。——类的实例化
1. this指针存在哪里?
——this指针是形参,实参是对象的地址,所以this指针跟普通参数一样存在栈里面,作为栈帧的一部分。
vs下面对this指针,进行的优化,对象的地址放在exc,exc存储this指针的值。
2. this指针可以为空吗?
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
首先我们可以排除A,不可能是编译错误。空指针是运行的问题,不会报编译的错误。
对象是.,指针是->. C
p是一个Date类型的空指针,然后我们通过p1去调用类的成员函数Print
- 大家可能会想,这里不是对空指针解引用了吗?怎么还运行正常啊?
那要告诉大家的是,我们不能看到->或者.就认为一定存在解引用,还是要根据具体情况进行分析。
我们上面说调用类成员函数时会进行一个隐式的传参,传的是当前调用成员函数的对象的地址,那现在的情况是什么,是不是传过去了一个空指针啊。
- 传参传空指针一定会出错吗?
是不是不一定啊,函数那边没有进行空指针的检查,那是不是只要不对空指针进行解引用就没问题啊。
而 Print 函数里面是不是只是打印了一个字符串 "Print()" ,并没有对空的this指针解引用,所以程序正常运行,没有问题。
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class A { public: void PrintA() { cout<<_a<
PrintA(); return 0; } 那这下是不是就要出问题了啊,因为Init函数里面是不是要通过this指针去找当前对象的成员变量(解引用了),但现在传过来的this指针是空指针,那对空指针解引用是不是程序就崩溃了。
左边没有解引用,右边有解引用,空指针不能解引用。——对空指针解用是会使运行崩溃。
总结:
不进行对函数的解引用(不对函数中成员变量进行调用)就不会使运行崩溃。
我们不能因为是空指针都是运行崩溃,我们得看是不是对其进行解引用,有没有进行函数内部的成员变量进行调用,如果只是进行打印内容是不会使运行崩溃的。
1.c语言实现
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
void StackDestroy(Stack * ps)
{
assert(ps);
if (ps->array)
{
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
void CheckCapacity(Stack* ps)
{
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity * 2;
DataType* temp = (DataType*)realloc(ps->array,
newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
ps->array = temp;
ps->capacity = newcapacity;
}
}
void StackPush(Stack* ps, DataType data)
{
assert(ps);
CheckCapacity(ps);
ps->array[ps->size] = data;
ps->size++;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->size;
}
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->size--;
}
DataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->array[ps->size - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->size;
}
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackPop(&s);
StackPop(&s);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackDestroy(&s);
return 0;
}
可以看到,在用C语言实现时,Stack相关操作函数有以下共性:
- 每个函数的第一个参数都是Stack*
- 函数中必须要对第一个参数检测,因为该参数可能会为NULL
- 函数中都是通过Stack*参数操作栈的
- 调用时必须传递Stack结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。
2. 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;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}
C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中 Stack * 参数是编译器维护的,C语言中需用用户自己维护。
这里可以清晰的可以看出其中的差别了。
祝我看似低矮,万尽山开。