1.类的定义
1.1 类定义格式
#include<iostream>
#include<assert.h>
using namespace std;
class stack
{
public:
//成员函数
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
void Push(int x)
{
array[top++] = x;
}
int Top()
{
assert(top > 0);
return array[top - 1];
}
void Destroy()
{
free(array);
array = nullptr;
capacity = top = 0;
}
//成员变量
private:
int* array;
int capacity;
int top;
};//分号不能省略
int main()
{
stack st;
//不传实参,默认形参为缺省值。
st.Init();
st.Push(1);
st.Push(2);
cout << st.Top() << endl;
st.Destroy();
return 0;
}
- 如图,定义了了一个名为 stack的类,类的定义和结构体的定义类似,只不过类里面还可以定义函数,而结构体不行,定义类是需要用到关键字 class ,再在后面加上 类的名字,然后是{}里面是类的主体,注意类定义介绍后的分号不能省略,类体中的内容称为类的成员,类中的变量称为类的属性或成员变量,类中的函数称为类的方法 或成员函数。
Data类
class Data
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;//
// 为了区分成员变量,⼀般习惯上成员变量
// 会加⼀个特殊标识,如_ 或者 m开头
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d;
d.Init(2026, 5, 16);
return 0;
}
2 . 为了区分成员变量,⼀般习惯上成员变量
会加⼀个特殊标识,如_ 或者 m开头 。
- C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类
4.定义在类⾯的成员函数默认为inline
- 类名就是类型,所以定义一个类 就可以如上图,Data d; Data 就是类型·。
1.2 访问限定符

1 . C++⼀种实现封装的⽅式,⽤类将对象的属性与⽅法结合在⼀块,让对象更加完善,通过访问权限 选择性的将其接口 提供给外部的⽤⼾使⽤。
-
public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访 问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。
-
访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有 访问限定符 ,作⽤域就到 } 即类结束。
4 . class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
5 . ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public
讲解:


- 这里就体现了public修饰的成员在类外可以直接访问。而被private修饰的成员在类外不可以直接访问。

- 访问限定符的作用域的规定,如图 中 public 的作用域,就是次public出现的位置到下一个 访问限定符出现的位置前,就是它的作用域。如果后面没有访问限定符,如private 后面没有访问限定符了,那么它的作用域就是到类的结束位置 } 前。
3**. c++ 规定struct 也可以定义 类,且内部也可以定义函数。但是我们平常不用这个,还是用class 定义类,它们两个定义的类 存在区别。如果class 定义的类的成员没有访问限定符作用,那就默认**
是private,而struct默认为 public。
struct

class


1.3 类域
我们之前说过 c++中有 四个域,全局域 ,局部函数域,命名空间域,类域,
1.类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时, 需要使⽤ :: 作 ⽤域操作符指明成员属于哪个类域。
如下

// 声明和定义分离,需要指定类域,需要用到域作用访问限定符。
同时类域的定义 也可以避免命名冲突。因为在不同的域里面可以定义同名变量 和函数
2**.类域影响的是编译的查找规则 ,如果 上面的 Init不指定类域Stack,那么默认回去 全局中找,那么编译时,找不到array等成员的声明/定义在哪⾥,就会报错。指定类域Stack,就是知 道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找**
2. 实例化
- ⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。
如上面的 stack s;Data d;
- 类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只 是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。
3. 一个类可以实例出多个对象,实例化出的对象,会占用实际的物理空间来存储成员变量。打个比方,就行我们实际生活中,使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多 少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房 ⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。

cpp
#include<iostream>
#include<assert.h>
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和d2
Date d1;
Date d2;
d1.Init(2026, 5, 17);
d1.Print();
d2.Init(2018,8,18);
d2.Print();
return 0;
}
2.2 对象⼤⼩
分析⼀下类对象中哪些成员呢?类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含 成员变量,那么成员函数是否包含呢?
⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令 存储在⼀个单独的区域(代码段),那么对象中⾮要存储的话,只能是成员函数的指针。再分析⼀下,对 象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量 _year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的 ,存储在对象 中就浪费了。如果⽤Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了 。这⾥需 要再额外哆嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址,调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在 运⾏时找,就需要存储函数地址,这个我们以后会讲解。
所以对象中就只存储成员变量,C**++规定类实例化的对象也要符合内存对⻬的规则。就想我们之前学习过的 结构体的内存对齐。**
内存对齐规则
1,第⼀个成员在与结构体偏移量为0的地址处
-
其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处
-
注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值,vs默认的对齐数为8
4. VS中默认的对⻬数为8
- 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
6.如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩ 就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。
学习完上述规则后,那我们来简单应用一下
cpp
#include<iostream>
using namespace std;
// 计算⼀下A/B/C实例化的对象是多⼤?
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
// 大小为 8byte
class B
{
public:
void Print()
{
//...
}
};// 大小为 1byte
class C
{
// 大小为 1byte
};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << endl;
return 0;
}
A类的大小 为什么为 8byte呢,首先我们知道 成员函数是不存储的,所以就只需考虑成员变量,char_ ch; int_i; 首先char _ ch 大小为1比默认对齐数8小,所以先对齐到 偏移量为1 的位置,然后 对齐 int i,因为大小为 4字节,而编译器默认对齐数为 8,所以取较小值 4,那么int i,需要从4的最小整数倍开始对齐,那么将从 偏移量为 4的位置开始对齐,往后占用4 个字节,总共占用 8个字节,而8个字节为最大对齐数 4的整数倍,所以最终对象的大小为 8个字节。
上⾯的程序运⾏后,我们看到没有成员变量的B和C类对象的⼤⼩是1,为什么没有成员变量还要给1个 字节呢?因为如果⼀个字节都不给,怎么表⽰对象存在过呢!所以这⾥给1字节,纯粹是为了占位标识 对象存在。
3. this指针
cpp
#include<iostream>
using namespace std;
class Data
{
public:
//void Init(Data* const this ,int year,int month,int day)
void Init(int year, int month, int day)
{
this->_year = year;;
this->_month = month;
this->_day = day;
}
//void Print(Data* const this)
void Print()
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
Data d2;
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
我们上面讲过,d1 和 d2 这两个对象的成员变量是相互独立的,但是成员函数缺确实相同的,那么当d1 调用 Init 和 Print时,是怎么知道是 d1调用的,还是d2调用的呢,
其实C++给了 ⼀个隐含的this指针解决这⾥的问题。
编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this 指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year, int month, int day)。
并且 成员函数的函数体中的 _year._month,_day 并不是 成员变量,因为类中的成员变量只是声明,并没有开辟空间,
有了this指针的话,当我们调用成员函数时,就会传对象的地址过去,这样就能区分究竟是d1调用还是d2调用了,

类的成员函数中访问成员变量,本质都是通过this指针访问的,
如Init函数中给_year赋值, this- >_year = year;
C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显 ⽰使⽤this指针

那么this指针是存在内存的那个区域中,this指针本质上是形参,而形参是存储在栈中的。
下⾯通过两个选择题测试⼀下前⾯的知识学得如何?
1.下⾯程序编译运⾏结果是() A、编译报错 B、运⾏崩溃 C、正常运⾏
cpp
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}

答案是正常运行,p->Print()看似是进行了解引用操作,实际上并没有,因为成员函数指针并没有存在对象里面,实际上是一句call指令,先把p给rcx这个寄存器,寄存器里面实际存的是对象的地址,而这里的p就是对象的地址,所以实际上没有解引用操作,所以是正常运行

如果代码为
cpp
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->_a;
return 0;
}
p->-a;那么就会导致程序崩溃,因为_a是成员变量存在对象里面,那么这里就会涉及到nullptr解引用的操作,导致程序崩溃。
2.下⾯程序编译运⾏结果是() A、编译报错 B、运⾏崩溃 C、正常运⾏
cpp
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
这里的运行结果 是程序崩溃,p->Print(),要调用这个函数要把p传给this指针,然后再call函数的地址,而函数的地址不在对象中,所以不涉及解引用的1操作,到这里是正常的,但是Print()函数里面涉及到 cout<< -a << endl;涉及到 访问 _a的操作,相当于 this->-a,但是由于this指针是nullptr所以存在对空指针的解引用,导致程序崩溃。

4. C++和C语⾔实现Stack对⽐
**⾯向对象三⼤特性:封装、继承、多态,**下⾯的对⽐我们可以初步了解⼀下封装。 通过下⾯两份代码对⽐,我们发现C++实现Stack形态上还是发⽣了挺多的变化,底层和逻辑上没啥变 化。
C实现Stack代码
cpp
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
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);
return 0;
}
C++实现Stack代码
cpp
#include<iostream>
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++封装的⼀种体现,这个是最重要的变化。
比如说,在c语言中,访问栈顶元素,只需要一行代码,那为什么要单独实现一个函数去管理,
于是就直接访问结构 , 这样访问s.[s.top - 1] ,这样就存在 很多缺陷,比如栈为空,top为0,就访问到 -1的位置,或者说 会写成 s.[s.top],这都是错误的。
而在c++中就不能这样访问,因为c++建议 成员变量都是私有的,就不能在外面随便访问,我们想让人访问的 ,就搞成公用,不想的就搞成私有,这样就实现出规范化管理
这⾥的封装的本质是⼀种更严格规范的管 理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后⾯还需要不断的去学习。
• C++中有⼀些相对**⽅便的语法** ,⽐如Init给的缺省参数会⽅便很多,
还有成员函数每次不需要传对象地 址,因为this指针隐含的传递了,⽅便了很多,
使⽤类型不再需要typedef ,⽤类名就很⽅便
• 在我们这个C++⼊⻔阶段实现的Stack看起来变了很多,但是实质上变化不⼤。等着我们后⾯看STL 中的⽤适配器实现的Stack,⼤家再感受C++的魅⼒。