
欢迎来到Harper·Lee的学习笔记!
博主主页传送门:Harper·Lee博客主页!
欢迎各位大佬交流学习!

本篇本章正式进入C++的类和对象部分,本部分知识分为三小节。
复习:
在之前学习的C语言是一种面向过程的语言,它关注的是求解问题的具体实现过程,一般通过函数调用逐步解决问题。就比如说我们以面向过程的方式分析淘米:
C++就是一种面向对象的语言,它将一个问题分成多个对象,更强调对象与对象之间的联系。在淘米煮饭这个例子中,一共有四个对象:米、水、锅、人。
整个过程中,这四个对象之间是交互完成任务的。在面向对象中,我们更强调对象之间的连续,并不太在意其内在是如何完成的。
#include
using namespace std;
class className//类的定义方式1——class
{
//成员函数
void Push(int x);
void Pop();
int Top();
//成员变量
int* a;//数组
int top;
int capacity;
};
//test.c
//在C语言中,一般都会使用typedef
struct Node1//结构体
{
struct Node1* next;//注意这里:struct Node1*才是类型名,才能用它定义变量next
int val;
};
//test.cpp
#include
using namespace std;
struct Node2//类的定义方式2——struct
{
Node2* next;//区别改变在这里,直接用类名,
int val;
void top();
};//这一中在C语言中是不能通过的!!!!C++兼容C语言,C语言不兼容C++!!!
int main()
{
//test.cpp
Node2 n1;//定义n1
struct Node2 n2;//定义n2,C语言的用法C++也支持!
return 0;
}
也可以进行声明和定义的分离(比如声明和定义在不同文件中)
**a. 声明和定义没有分离。**注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
//1. 声明和定义没有分离:
#include
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
month = month;
day = day;
}
private:
int _year;
int _month;
int _day;
};
b.声明和定义分离。声明放在头文件(.h)中,定义放在源文件(.cpp)中。 声明和定义分离需要指定类域,就使用了::域作用限定符。
//test.h
#include
using namespace std;
class Date
{
public:
void Init(int year, int month, int day);//声明
private:
int year;
int month;
int day;
};
//test.cpp
void Date::Init(int year, int month, int day)//定义
//声明和定义分离需要指定类域,就使用了域作用限定符
{ //Init是类Date的成员函数,只是声明和定义分离了而已
year = year;
month = month;
day = day;
}
在C++中有三种访问限定符:public、private、protected 。
#include
using namespace std;
class Date
{
public://可以访问
void Init(int year, int month, int day)//非成员变量
{
_year = year;
_month = month;
_day = day;
}
private://不可以访问
int _year;//成员变量
int _month;
int _day;
};
从下面的代码可以看出,类中的成员变量和非成员变量之间存在着重名冲突。
#include
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)//非成员变量
{
year = year;
month = month;
day = day;
}
private:
int year;//成员变量
int month;
int day;
};
C++标准并没有规定成员变量的命名规则。为了区分出成员变量,惯例上,我们在定义成员变量时会对其进行特定地修饰,比如_变量名或者是m_变量名或者变量名_。 不同公司可能都有一套自己的命名规则,但目的只是为了区分出成员变量。
#include
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)//非成员变量
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;//成员变量
int _month;
int _day;
};
变量的声明:告知变量的名称、类型,没有开辟空间;
变量的定义:开辟了空间。如果是定义,就有空间,就可以直接使用/访问。如下面main函数中的_year。
#include
using namespace std;
class Date
{
public:
void Init(int year, int month, int day);//声明
private:
int _year;//是定义还是声明????————声明
int _month;
int _day;
};
//test.cpp
void Date::Init(int year, int month, int day)//定义
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date::_year = 2024;//error,所以不能直接使用变量,说明没有开辟空间,_year是声明
return 0;
}


类:是对对象进行描述的、就像模型一样的东西。
类限定了类有哪些成员,类中的成员变量是一种声明,并没有分配实际的内存空间来存储它。就比如我们可以根据一张图纸(没有实际空间),创建出好几座不一样的房屋(有实际空间)。二者不一样的房子就是类实例化出的对象。
用类名定义对象的方式,称为类的实例化。单独的类并不占据实际空间。而且一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类的成员变量。
#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 main()
{
//实例化出d1,d2两个对象
Date d1;//Date类名就是一个类型,直接用来定义
Date d2;
d1.Init(2024, 7, 10);/
d1.Print();
d2.Init(2024, 1, 1);
d2.Print();
return 0;
}
#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 main()
{
Date d1;
Date d2;
d1.Init(2024, 7, 10);//使用 . 操作符访问成员对象
d1.Print();
d2.Init(2024, 1, 1);
d2.Print();
return 0;
}
上面的代码,透过汇编观察可以发现,类定义的不同对象虽然成员变量不同,就需要各自存储他们;但是调用的成员函数其实是一样的(地址是一样的),如果在每个对象里面都存储一份,就会有空间的重复浪费。(例如:用类实例化出100个对象,每个成员函数都要重复地存储100次,实属浪费!)
其实,函数指针是不需要存储的。函数指针是一个地址,调用函数被汇编成汇编指令[call地址、jmp指令]。编译器在编译链接时就要找到函数的地址,不是在运行时找;而动态多态是在运行时找的,就需要存储函数地址。——跳转到下面的this指针
总结:


C++的内存对齐规则和C语言结构体中的内存对齐规则一模一样!
· 第一个成员在与结构体偏移量为0的地址处;
· 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处;
· 注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值;
· VS中默认的对齐数为8;
· 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍;
• 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小;
·就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍;
C语言:自定义类型——结构体(✿༺小陈在拼命༻✿)(这一部分知识可以参考这位优秀博主)
计算A实例化对象的大小:
#include
using namespace std;
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
int main()
{
A a;
cout << sizeof(a) << endl;
cout << &a << endl;
return 0;
}
运行结果:
画图分析:
Q:为什么要内存对齐?
A:CPU读取内存数据时,并不是从任意位置开始读取的,读取与CPU的字长、数据总线有关,
如果CPU可以从任一位置读取,那么内存对齐规则不仅麻烦,还浪费时间。为什么还要有对齐规则呢?
读取数据与数据总线有关,32根数据总线(4Byte,数据总线数量与机器型号有关)----计算机组成原理。规定:从起始点开始,不管数据有多少,CPU一次都读取32个数据,但是这里面也许只有1个数据是CPU真正想读取的。
综合来看,内存对齐规则减少访问次数、提高了CPU的效率。
空类:类中只有成员函数或者什么也没有时,这个类就叫做空类。
//计算⼀下B/C实例化的对象是多⼤?
#include
using namespace std;
class B
{
public:
void Print()
{
//...
}
};
class C
{};
int main()
{
B b;
C c;
cout << sizeof(b) << endl;
cout << &b << endl;
cout << sizeof(c) << endl;
cout << &c << endl;
return 0;
}
运行结果:
B和C都是空类。首先类对象的大小不能为0,否则不能表示该对象存在过,而且还要通过B对象开空间去实例化呢。因此,C++给空类1Byte的空间用来占位标记这个类的对象的存在,实际操作中的使用性也很少。
先观察这段代码:
#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 main()
{
Date d1;
Date d2;
d1.Init(2024, 7, 10);//使用 . 操作符访问成员对象
d1.Print();
d2.Init(2024, 1, 1);
d2.Print();
return 0;
}
运行结果:
根据前面 3.2.1 的分析可知,类中的成员函数并没有存储在类的对象中,而且函数体中并没有关于不同对象的区分,那么函数是如何知晓自己应该访问哪个对象的呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象)(因此,this指针是一个当前类类型的指针,传参就要传入&类名定义的对象名,如 &d1)。
在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。这个隐藏的指针参数就是this指针。
类的成员函数中访问成员变量,本质都是通过this指针来访问的(编译时编译器自动会处理)。如Init函数中给_year 赋值:this -> _year = year;
加入this指针后的原型代码如下面的注释:
#include
using namespace std;
class Date
{
public:
//原型:void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//原型:void Print(Date* const this)//const修饰的是this
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
//原型可以显示写:cout << this -> _year << "-" << this -> _month << "-" << this -> _day << endl;
}
private:
int _year;
int _month;
int _day;
//this->_year = year;原型
//this->_month = month;
//this->_day = day;//这个this->可以显示地写出来,因为调用函数体中的this指针
};
int main()
{
Date d1;
Date d2;
d1.Init(2024, 7, 10);//原型:d1.Init(&d1, 2024, 7, 10);
d1.Print(); //原型:d1.Print(&d1);
d2.Init(2024, 1, 1); //原型:d2.Init(&d2,2024,1,1);
d2.Print(); //原型:d2.Print(&d2);
return 0;
}

#include
using namespace std;
class A
{
public:
void Print()
{
//cout << this << endl;//输出空指针0000000,不会报错,因为没有使用!!
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;//p是一个nullptr指针空值,类型为 A* ,是类类型
//mov ecx p
p->Print();//call 地址//p->Print(p);//这里不需要取地址!!!
//p -> _a = 1;//error这个才是空指针解引用,_a存储在对象里面的(_a是public时)
return 0;
}
运行结果:
,
???
**程序正常运行,**分析过程:
#include
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;//这里的_a是通过this指针解引用访问的,空指针解引用!!!!
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
//p->_a = 1;//error,这里就是在类外面访问的,本来是被限制了的
return 0;
}
运行结果:
程序运行崩,过程分析:
访问对应的成员变量,会传递对应对象的地址。而这里的地址为nullptr,通过nullptr->_a引起程序崩溃。
面向对象有三大特性:封装、继承、多态。
封装的概念: 用类将对象的属性(数据)与操作数据的方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
封装本质上是一种管理,让用户更方便使用类。 比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
类也是一样,我们使用类将数据和方法都封装起来。不想对外开放的就用 protected/private 封装起来,用 public 封装的成员允许外界对其进行合理的访问。 所以封装本质上是一种管理。
//Stack.c #include'#include #include #include typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST* ps) { assert(ps); ps->a = NULL; ps->top = 0; ps->capacity = 0; } void STDestroy(ST* ps) { assert(ps); free(ps->a); ps->a = NULL; ps->top = ps->capacity = 0; } void STPush(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"); return; } ps->a = tmp; ps->capacity = newcapacity; } ps->a[ps->top] = x; ps->top++; } bool STEmpty(ST* ps) { assert(ps); return ps->top == 0; } void STPop(ST* ps) { assert(ps); assert(!STEmpty(ps)); ps->top--; } STDataType STTop(ST* ps) { assert(ps); assert(!STEmpty(ps)); return ps->a[ps->top - 1]; } int STSize(ST* ps) { assert(ps); return ps->top; } int main() { ST s; STInit(&s); STPush(&s, 1); STPush(&s, 2); STPush(&s, 3); STPush(&s, 4); while (!STEmpty(&s)) { printf("%d\n", STTop(&s)); STPop(&s); } STDestroy(&s);//s.a[s.top]可以直接访问栈顶元素,但是不规范 //但是这种访问方式并不好,万一栈是空栈时,越界! //所以用C语言这样的实现不怎么规范,而且存在风险!!! return 0; } 运行
//Stcak.cpp
#include
using namespace std;
typedef int STDataType;
class Stack
{
public:
// 成员函数
void Init(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
void Pop()
{
assert(_top > 0);
--_top;
}
bool Empty()
{
return _top == 0;
}
int Top()
{
assert(_top > 0);
return _a[_top - 1];
}
void Destroy()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
// 成员变量
STDataType * _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
while (!s.Empty())
{
printf("%d\n", s.Top());
s.Pop();
}
s.Destroy();
return 0;
}
C++中数据和函数都放到了类里面,通过访问限定符进性了限制,不能再随意通过对象直接修改数据,这是C++封装的⼀种体现,这个是最重要的变化。这里的封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。
想想我们是如何管理陕西省的兵马俑的。我们若什么都不管,兵马俑就被随意破坏了。所以我们建立了一座房子将兵马俑封装起来。但是我们封装的目的不是为了不给别人看,所以我们开放了售票通道,人们可以通过买票突破封装,在合理的监管机制下进去参观。

创作不易,喜欢的uu记得三连支持一下哦!

