目录
[3.1 类的两种定义方式](#3.1 类的两种定义方式)
[3.2 成员变量命名的风格](#3.2 成员变量命名的风格)
[4.1 访问限定符](#4.1 访问限定符)
[4.2 封装](#4.2 封装)
[7.1 类的对象的储存方式](#7.1 类的对象的储存方式)
[7.2 类的大小的计算](#7.2 类的大小的计算)
[7.3 结构体内存对齐规则](#7.3 结构体内存对齐规则)
[8.1 this指针的引出](#8.1 this指针的引出)
[8.2 this指针的特性](#8.2 this指针的特性)
[8.3 C语言和C++实现Stack的对比](#8.3 C语言和C++实现Stack的对比)
一、面向过程和面向对象
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。


C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。


二、类的引入
第一个类的引入:结构体的升级。在C语言结构体中只能定义变量,在C++结构体中不仅可以定义变量,也可以定义函数。不过C++兼容C语言,C语言中结构体的用法在C++文件中可以继续使用,同时在C++中,结构体也升级成了类。
例如:用C语言和C++中的结构体实现栈
cpp
#C语言
struct Stack
{
int* a;
int top;
int capacity;
};
#C++
struct Stack
{
//成员函数
void Init()
{
_a = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
//扩容
if (_top == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
_a = (int*)realloc(_a, sizeof(int) * newcapacity);
_capacity = newcapacity;
}
_a[_top++] = x;
}
void Destroy()
{
if (_a)
{
free(_a);
}
_a = nullptr;
_top = _capacity = 0;
}
//成员变量
int* _a;
int _top;
int _capacity;
};
并且C语言和C++的结构体在定义变量时也会有些区别。
cpp
//结构体的用法
struct Stack st1;
//类的用法
Stack st2;
//访问
st2.Init();
st2.Push(1);
st2.Push(2);
st2.Push(2);
st2.Push(1);
st2.Push(2);
st2.Destroy();
三、类的定义
类的定义和结构体类似。
cpp
class classname
{
//类体:由成员变量和成员函数组成
};
class是定义类的关键字,classname是类的名字,{}中为类的主体,注意类定义结束时后面的分号不能省略。
类体中的内容称为类的成员:类中的变量成为类的属性或成员变量;类中的函数称为类的方法或成员函数。
3.1 类的两种定义方式
1.声明和定义全部放在类体中。不过需要注意的是,如果类的成员函数直接在类里面定义,会默认为内联函数。

2.类的声明放在.h文件中,成员函数定义放在.cpp文件中。不过需要注意的是,成员函数名前需要加类名。

一般情况下更期望采用第二种方式。
3.2 成员变量命名的风格
当成员变量和成员函数的形参名称一致时,对形参的修改不会影响成员变量。
例如:
cpp
//日期类
class Data
{
public:
//当成员函数的形参名称和成员变量名称一致时
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
private:
int year;
int month;
int day;
};
所以一般建议成员变量前可以加_,或者在成员变量前加字母m代表member,可以区分成员变量,但这不是C++的严格规定。
cpp
class Data
{
private:
int _year;
int _month;
int _day;
};
class Data
{
private:
int myear;
int mmonth;
int mday;
};
四、类的访问限定符及封装
4.1 访问限定符
C++实现封装的方式:用类将对象属性与方法结合在一块,让对象更加的完善,通过访问权限选择性的将其接口提供给外部的用户使用。

访问限定符的说明:
1.public修饰的成员在类外可以直接被访问。
2.protected和private修饰的成员在类外不能直接被访问。
3.访问权限的作用域是从访问限定符出现的位置到下一个访问限定符出现的位置。
4.如果后面没有访问限定符,作用域就到 }; 结束。
5.class的默认访问权限是private,struct的默认访问权限是public。
6.类内部的成员函数不受访问限定符的限制。
C++中struct和class的区别:
C++需要兼容C语言,,所以C++中的struct可以继续当成结构体使用。另外C++中的struct还可以用来定义类。和class定义类是一样的,不过区别是struct定义的类默认的访问限定符是public,class定义的类默认访问权限是private。
4.2 封装
五、类的作用域
"{}"内部基本都可以看作域,例如命名空间域、函数作用域、循环的循环体等。类同时也定义了一个域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
cpp
//类的作用域
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
//在类外定义时需要用域操作符进行指定
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age;
}
六、类的实例化
用类类型创建对象的过程,称为类的实例化。
1.类是用来对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员。类是存放在文件系统中的,定义一个类时并不会分配实际的内存空间进行储存。
2.一个类可以实例化出多个对象,实例化的对象会占用实际的物理空间,储存类的成员变量。
cpp
class Person
{
public:
char _name[20]; //名字
char _gender[3]; //性别
int _age; //年龄
};
//类在没有进行实例化时没办法直接通过类名访问成员
Person._age = 0; //error C2059: 语法错误:"."
//类的实例化,实例化后的对象可以直接访问公开的成员
Person p;
p._age = 0;
3.类类似于设计建筑时的设计图纸,类实例化出的对象类似于按照设计图建造出来的建筑或房子。

七、类对象模型
7.1 类的对象的储存方式
类实例化出的对象只保存成员变量。成员函数存放在公共的代码段中。
cpp
class Person
{
public:
//成员函数
void Init(const char* name, const char* gender, int age)
{
strcpy(_name, name);
strcpy(_gender, gender);
_age = age;
}
void PrintInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
//.....
private:
//成员变量
char _name[20]; //名字
char _gender[3]; //性别
int _age; //年龄
};

注:成员变量是每个对象独有的,成员函数是公用的,比如说p1._name和p2._name并不是同一个成员变量,但p1.Init()和p2.Init()使用的是同一个成员函数。
7.2 类的大小的计算
类的大小的计算和结构体大小的计算方法相同,都需要用到结构体内存对齐的规则,并且计算类的大小的时候不会去计算类内部成员函数的大小,只会计算类内部成员变量的大小。此外,当一个类里面没有定义任何成员变量时,把这个类看做空类,默认大小为1个字节。
cpp
class A1
{
public:
void Init()
{}
private:
int _a;
};
class A2
{
public:
void Init()
{}
};
class A3
{
};
cout << sizeof(A1) << endl; //4
cout << sizeof(A2) << endl; //1
cout << sizeof(A3) << endl; //1
A2 aa2;
A2 aaa2;
注:A2和A3被分配1个字节,但这1个字节并不会存储数据,只是占位 ,表示对象存在过。aa2和aaa2都是被A2这个类实例化出来的对象,说明aa2和aaa2被定义了,被定义就必须要开空间,因此会给空类分配1个字节表示空类实例化的对象存在。
7.3 结构体内存对齐规则
1.第一个成员对齐在结构体偏移量为0的地址处。
2.其他成员要对齐到相应对齐数的整数倍的地址处。
cpp
对齐数计算方法:
编译器在32位平台(X86)上默认对齐数是4,64位平台上(X64)默认对齐数是8。
对齐数=编译器默认对齐数和该成员类型的大小两者中较小值。
3.结构体总大小必须为最大对齐数(默认对齐数和所有其他成员对齐数中最大的那个)的整数倍。
4.如果嵌套了结构体,那么结构体要对齐到结构体自身最大对齐数的整数倍处。数组的对齐数是数组类型的大小和默认对齐数两者中较小的那个。
5.结构体内存对齐是为了让CPU访问更方便,减少访问次数,提高访问效率,本质上和利用空间换时间的性质很像。
6.控制默认对齐数:#pragma pack(要设置的默认对齐数)。
八、this指针
8.1 this指针的引出
定义一个Date类:
cpp
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 main()
{
Date d1;
Date d2;
d1.Init(2023, 7, 20);
d2.Init(2023, 7, 21);
d1.Print();
d2.Print();
return 0;
}
引出问题:Date类中有两个成员函数Init和Print,成员函数函数体内并没有关于如何去区分不同对象,那么当d1去调用Init时,Init函数是如何知道该去设置d1对象而不是去设置d2对象的呢?
C++中通过引入了this指针来解决这个问题,即:C++编译器会给每个"非静态成员函数"增加了一个隐藏的指针参数,让该指针指向当前调用该函数的对象,在函数体内所有"成员变量"的操作,都是通过该指针去访问。只不过所有操作对用户来说都是透明的,即不需要用户去显示传递,编译器会自动完成this指针的传递。
成员函数可以显示表示为:
cpp
//this指针的显示表示
void Init(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Date* const this)
{
cout << this->_year << " " << this->_month << " " << this->_day << endl;
}
//同理,在调用的地方也会被编译器进行修改
d1.Init(&d1, 2023, 7, 20);
d2.Init(&d2, 2023, 7, 20);
d1.Print(&d1);
d2.Print(&d2);
注:this指针在实参和形参的位置不能显示写,编译器会自动进行处理。
8.2 this指针的特性
1.this指针的类型:类类型* const,即成员函数中无法对this指针进行修改,但可以修改this指针指向的内容。
2.this指针无法在实参和形参位置显示写,只能在成员函数内部使用。this指针是C++中的一个关键字。
cpp
//不能显示在形参位置写出this指针
void Init(Date* const this, int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//但可以在成员函数内部显示访问this指针
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
3.this指针是指向对象的指针,本质上是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存放this指针。
4.this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。

5.this指针实际上存放在栈区里面。
6.this指针可以为nullptr。
cpp
class A
{
public:
void Print1()
{
cout << "Print1()" << endl;
}
void Print2()
{
cout << _a << endl;
}
private:
//在声明处设置缺省参数
int _a=1;
};
int main()
{
A* p = nullptr;
//可以正常调用,会被编译器自动转化为Print1(nullptr)
p->Print1();
//同样可以正常调用,但可能会因为不同编译器的优化产生不同的结果
(*p).Print1();
//会崩溃,因为Print2中有成员变量的访问操作
p->Print2();
return 0;
}
7.不同成员函数内的this是不同的变量,因为this在不同成员函数内都是作为形参的存在,当同一个对象调用不同成员变量时,那么成员变量内的this指针的值就一样,不同对象调用不同的成员函数时,this指针的值就不一样,this指针指向调用成员函数的对象,是该对象的地址。

8.3 C语言和C++实现Stack的对比
C语言实现Stack:
cpp
#include<assert.h>
//数据
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
//方法
void StackInit(ST* pst)
{
assert(pst);
//...
}
void CheckCapacity(ST* pst)
{
assert(pst);
//...
}
void StackPush(ST* pst, STDataType x)
{
assert(pst);
//...
}
//...
void StackDestroy(ST* pst)
{
assert(pst);
//...
}
int main()
{
ST st;
StackInit(&st);
StackPush(&st,1);
//...
StackDestroy(&st);
return 0;
}
可以看到,在用C语言实现Stack栈时相关操作会有以下共性:
cpp
每个函数的第一个参数都是ST*
函数中必须要对第一个参数进行检查,防止其为NULL
函数中都是通过ST*来操作栈的
调用时必须传递ST结构体变量的地址
并且结构体中只能定义存放数据的结构,数据操作的方法不能放在结构中,即数据和方法是分离的。而且实现相较会更加复杂,会涉及到大量的指针操作,更容易出错。
C++实现Stack:
cpp
typedef int STDataType;
class Stack
{
public:
//方法
void Init()
{
//...
}
void CheckCapacity()
{
//...
}
void Push(STDataType x)
{
//...
}
//...
void Destroy()
{
//...
}
private:
//数据
STDataType* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
st1.Init();
st1.Push(1);
//...
st1.Destroy();
return 0;
}
C++通过类可以将数据和操作数据的方法完美结合,通过访问权限的设置可以控制方法可以在类外被调用访问,数据无法在类外被调用访问,即封装。而且C++中Stack*参数是由编译器自动维护的,可以降低失误的产生。