
大家好,我是你们的小小风呀!上一篇我们搞定了 C++ 的基础语法,今天我们正式进入面向对象的世界啦!
这是 C++ 最核心的特性,之前我们写的都是面向过程的代码,按步骤一步步执行,但是当程序变大之后,这样的代码会越来越难维护。而面向对象,就是把现实世界的事物抽象成代码里的类和对象,把数据和操作数据的方法打包在一起,让代码更清晰、更安全、更好维护。
今天我们先从最基础的类的定义、实例化开始,最后我们还会用 C 和 C++ 分别实现一个栈,直观感受一下面向对象的优势!全程大白话 + 简单代码,新手也能看懂!
一、类的定义
我们之前学 C 语言的时候,用过结构体,它可以把一组变量打包在一起,但是它只能放数据,不能放操作数据的函数。而 C++ 的类,就是结构体的超级升级版,它不仅能放数据(属性),还能放操作数据的函数(行为),把它们打包成一个整体,这就是封装的基础。
1. 类定义格式
基础概念
类的定义语法很简单,用++class++关键字,后面跟类名,然后大括号里放类的成员,最后结尾必须加一个分号!这是新手最容易忘的错误,一定要记牢!
类的成员分为两种:
-
成员变量:也就是类的属性,描述这个类的特征,比如学生的姓名、年龄
-
成员函数:也就是类的方法,描述这个类能做什么,比如学生能学习、能吃饭
还有一个行业惯例,成员变量的名字一般会加一个_前缀,比如_name、_age,用来和普通的局部变量、形参区分开,这个不是语法强制的,但是大家都这么写,方便阅读。
示例 1:定义一个简单的学生类,了解类的基本格式
这个例子会帮你快速了解类的基本结构,我们定义一个学生类,包含学生的属性和行为。
cpp
#include <iostream>
#include <string>
using namespace std;
// 定义学生类:class是关键字,Student是类名
class Student {
public:
// 成员变量:学生的属性,加_前缀区分
string _name;
int _age;
// 成员函数:学生的行为
void study() {
cout << _name << "正在学习C++!" << endl;
}
}; // 注意!结尾必须加分号!新手最容易忘!
int main() {
// 用类创建对象,也就是实例化,类名直接当类型用
Student s;
// 给对象的属性赋值
s._name = "小明";
s._age = 18;
// 调用对象的方法
s.study();
return 0;
}
运行结果:
cpp
小明正在学习C++!
你看,是不是很简单?我们把学生的名字、年龄,还有他能做的学习行为,都打包在了一个类里,用的时候直接创建对象,调用方法就行,比 C 语言的结构体好用太多了。
2. 访问限定符
基础概念
刚才我们的例子里,所有成员都是 public 的,也就是外部可以直接访问修改,但是这样的话,外部可以随便给年龄改个负数,比如s._age = -10,这显然不合理,对吧?
所以 C++ 提供了访问限定符,用来控制类的成员能不能被外部访问,实现封装的核心思想:隐藏内部的细节,只暴露必要的接口,保证数据的安全性。
C++ 有三种访问限定符:
|---------------|---------|---------|---------------------------|
| 限定符 | 类外部能否访问 | 类内部能否访问 | 核心用途 |
| public(公有) | ✅ 可以 | ✅ 可以 | 暴露对外的接口,比如我们要让外部调用的方法 |
| private(私有) | ❌ 不可以 | ✅ 可以 | 隐藏内部的细节,比如成员变量,不让外部随便改 |
| protected(保护) | ❌ 不可以 | ✅ 可以 | 用于继承,子类可以访问,外部不行,新手暂时不用深入 |
还有一个关键规则:访问限定符的作用域,是从它出现的位置,到下一个限定符出现的位置 ,或者到类结束。而且,用class定义的类,默认的访问权限是 private,而用struct定义的话,默认是 public,这个新手很容易踩坑!
class-->默认private
struct-->默认public
示例 2:访问限定符的使用,理解封装的意义
这个例子会展示怎么用访问限定符实现封装,把年龄藏起来,只通过接口修改,还能做合法性校验。
cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
// 私有成员:外部不能直接访问,隐藏内部数据
string _name;
int _age;
public:
// 公有接口:外部可以调用,用来操作私有成员
// 设置姓名的方法
void set_name(string name) {
_name = name;
}
// 设置年龄的方法,还能做合法性校验!
void set_age(int age) {
// 年龄不能小于0,也不能大于150,无效的话就设为0
if (age >= 0 && age <= 150) {
_age = age;
} else {
cout << "年龄不合法!" << endl;
_age = 0;
}
}
// 获取姓名和年龄的方法
string get_name() {
return _name;
}
int get_age() {
return _age;
}
void study() {
cout << _name << "今年" << _age << "岁,正在学习C++!" << endl;
}
};
int main() {
Student s;
// ❌ 错误!_age是私有成员,外部不能直接访问!
// s._age = 18;
// ✅ 正确,通过公有接口来设置
s.set_name("小明");
s.set_age(18);
cout << "姓名:" << s.get_name() << endl;
cout << "年龄:" << s.get_age() << endl;
s.study();
// 测试无效年龄
cout << endl << "测试无效年龄:" << endl;
s.set_age(200);
cout << "年龄:" << s.get_age() << endl;
return 0;
}
运行结果:
cpp
姓名:小明
年龄:18
小明今年18岁,正在学习C++!
测试无效年龄:
年龄不合法!
年龄:0
你看,这就是封装的魔力!我们把内部的成员变量藏起来,外部不能随便改,只能通过我们提供的接口来操作,这样我们就能在接口里做校验,避免无效的数据,保证了数据的安全性,这比把所有成员都设为 public 要安全太多了。
3. 类域
基础概念
我们之前学过,命名空间有自己的作用域,类也一样!类定义了一个独立的作用域,叫做类域,类里面所有的成员都在这个域里,和外部的名字不会冲突。
这就意味着,如果我们要在类的外面定义类的成员函数,就必须用::作用域解析符,告诉编译器,这个函数是属于这个类的,不然编译器会把它当成全局函数,找不到类里的私有成员。
示例 3:类域的使用,学会类内声明类外定义成员函数
这个例子会展示怎么把成员函数的声明和定义分开,这在大项目里很常用,头文件放声明,源文件放定义。
cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string _name;
int _age;
public:
// 类内只写函数的声明,不写实现
void set_name(string name);
void set_age(int age);
string get_name();
int get_age();
void study();
};
// 类外定义成员函数,必须加Student::,指明这是Student类域里的函数
// 不然编译器会当成全局函数,找不到私有成员!
void Student::set_name(string name) {
_name = name;
}
void Student::set_age(int age) {
if (age >= 0 && age <= 150) {
_age = age;
} else {
cout << "年龄不合法!" << endl;
_age = 0;
}
}
string Student::get_name() {
return _name;
}
int Student::get_age() {
return _age;
}
void Student::study() {
cout << _name << "今年" << _age << "岁,正在学习C++!" << endl;
}
int main() {
Student s;
s.set_name("小红");
s.set_age(20);
s.study();
return 0;
}
运行结果:
cpp
小红今年20岁,正在学习C++!
你看,这样我们就把声明和定义分开了,代码更整洁,这就是类域的作用,它让类的成员和外部的成员隔离开,不会冲突。
二、类的实例化
讲完了类的定义,我们来讲怎么用类创建对象,这个过程就叫做实例化。
1. 实例化的概念
基础概念
类其实只是一个模板,一个蓝图,它是抽象的概念,本身不占用内存。比如我们定义的 Student 类,只是定义了学生有什么属性、什么行为,但是它本身没有具体的姓名、年龄,也不占内存。
而实例化,就是用这个模板,创建出具体的对象,对象才是真实存在的,会占用内存。一个类可以实例化出无数个对象,每个对象都有自己独立的成员变量,但是共享类的成员函数。
打个比方,类就像是月饼的模子,对象就是用模子做出来的月饼,一个模子可以做很多月饼,每个月饼都有自己的大小、口味,但是它们都是按照同一个模子做出来的。
示例 4:类的实例化,一个类创建多个对象
这个例子会展示怎么用一个类创建多个独立的对象,每个对象有自己的属性。
cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string _name;
int _age;
public:
void set_info(string name, int age) {
_name = name;
_age = age;
}
void show() {
cout << "姓名:" << _name << ",年龄:" << _age << endl;
}
};
int main() {
// 实例化两个学生对象,小明和小红
Student s1;
Student s2;
// 给小明赋值
s1.set_info("小明", 18);
// 给小红赋值
s2.set_info("小红", 20);
// 两个对象的属性是独立的,互不影响
cout << "小明的信息:";
s1.show();
cout << "小红的信息:";
s2.show();
return 0;
}
运行结果:
cpp
小明的信息:姓名:小明,年龄:18
小红的信息:姓名:小红,年龄:20
你看,我们用同一个 Student 类,创建了两个对象,它们的属性是独立的,修改 s1 的属性不会影响 s2,但是它们都用同一个 show 函数,这就是实例化的作用。
2. 对象大小
基础概念
很多新手会好奇,一个对象的大小是怎么算的?其实很简单,对象的大小,只算成员变量的大小 ,成员函数是不算的!因为成员函数存在代码段,整个类共享,每个对象不用存一份,不然如果有 1000 个对象,每个对象都存一份函数的代码,那也太浪费内存了。
而且,对象的内存对齐规则,和 C 语言的结构体是完全一样的!因为对象本质就是升级版的结构体而已。对齐规则我们之前其实提过,再给新手复习一下:
-
第一个成员,偏移地址是 0
-
其他成员,要对齐到它的对齐数的整数倍地址,对齐数是「成员自身大小」和「编译器默认对齐数」的较小值,VS 默认是 8,GCC 默认是 4
-
整个对象的总大小,必须是最大对齐数的整数倍
还有一个特殊情况,空类,也就是没有成员变量的类,它的大小是 1 字节,用来占位,标识这个对象存在,不然就没有大小了。
示例 5:对象大小的计算,理解内存对齐
这个例子会帮你理解对象的大小是怎么算的,还有内存对齐的规则。
cpp
#include <iostream>
using namespace std;
// 空类,没有成员变量
class Empty {};
class A {
private:
char _c; // 1字节
int _i; // 4字节
public:
void func() {} // 成员函数,不占对象的大小
};
int main() {
// 空类的大小是1字节,占位用
cout << "空类的大小:" << sizeof(Empty) << endl;
// A类的大小:char1字节,然后填充3字节,然后int4字节,总共8字节
cout << "A类对象的大小:" << sizeof(A) << endl;
return 0;
}
运行结果:
cpp
空类的大小:1
A类对象的大小:8
是不是很清楚?成员函数不占对象的大小,只有成员变量占,而且遵循内存对齐的规则,和结构体一模一样。
3. this 指针
基础概念
刚才我们说,多个对象共享同一个成员函数,那函数怎么知道,当前操作的是哪个对象的成员变量呢?比如 s1.study () 和 s2.study (),调用的是同一个函数,为什么一个打印小明,一个打印小红?
这就要用到this 指针 了!编译器会自动给每个非静态成员函数,加一个隐含的参数 ,就是 this 指针,这个指针指向调用这个函数的对象。当你访问成员变量的时候,其实是this->成员变量,只是我们不用写,编译器自动帮我们加了。
this 指针的特性:
-
它是隐含的,我们不能在参数列表里显式写它,但是可以在函数里用它
-
它是
const指针,不能修改它的指向,也就是不能把 this 改成别的对象的地址 -
它是自动传的,调用函数的时候,编译器会把对象的地址当成 this 指针的实参传进去
示例 6:this 指针的作用,解决成员变量和形参同名的问题
这个例子会展示 this 指针的用法,最常用的就是解决成员变量和形参同名的问题。
cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string _name;
int _age;
public:
// 形参名和成员变量名一样了,这时候用this指针区分
void set_info(string _name, int _age)
{
// this->成员,就是当前对象的成员,右边的是形参
this->_name = _name;
this->_age = _age;
}
//也可以直接省略this,让编译器自动识别,但要保证函数的形参和类的成员变量名不一样
//void set_info(string name, int age)
//{
// _name = name;
// _age = age;
//}
void show() {
// 其实这里也隐含了this,相当于this->_name
cout << "姓名:" << _name << ",年龄:" << _age << endl;
// 我们也可以显式用this,比如打印当前对象的地址
cout << "当前对象的地址:" << this << endl;
}
};
int main() {
Student s1;
Student s2;
s1.set_info("小明", 18);
s2.set_info("小红", 20);
cout << "s1的地址:" << &s1 << endl;
s1.show();
cout << endl;
cout << "s2的地址:" << &s2 << endl;
s2.show();
return 0;
}
运行结果:
cpp
s1的地址:0x7ffd7b9ff1b0
姓名:小明,年龄:18
当前对象的地址:0x7ffd7b9ff1b0
s2的地址:0x7ffd7b9ff1a0
姓名:小红,年龄:20
当前对象的地址:0x7ffd7b9ff1a0
你看,是不是很明显?调用 s1 的函数的时候,this 指针就指向 s1,调用 s2 的函数的时候,this 就指向 s2,所以函数就能区分开,操作的是哪个对象的成员了,这就是 this 指针的作用。
三、实战练习:用 C 和 C++ 分别实现栈
讲了这么多理论,我们来做个实战练习,分别用 C 语言和 C++ 实现一个栈,直观感受一下面向对象的优势,你就能明白,为什么我们要用类了。
1. C 语言实现栈(面向过程)
C 语言里,我们要实现栈,只能用结构体存数据,然后写一堆全局的函数,操作这个结构体,数据和函数是分离的,而且结构体的成员外部可以随便改,很不安全。
示例 7:C 语言实现栈,对比面向过程的写法
这是 C 语言的栈实现,我们来看看它的写法。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// C语言的结构体,只能存数据
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
//栈的初始化
void STInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
//栈的销毁
void STDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 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;
}
你看,C 语言的写法,数据和函数是分离的,每个函数都要传结构体的指针,很麻烦,而且外部可以随便修改结构体的内部成员,很容易把数据改坏,没有任何保护。
2. C++ 实现栈(面向对象)
而用 C++ 的类来实现栈,我们把数据和函数都封装在类里,数据设为私有,外部不能随便改,函数设为公有,外部调用的时候不用传指针,直接调用就行,非常方便,而且安全。
示例 8:C++ 实现栈,感受面向对象的封装
这是 C++ 的栈实现,对比一下,是不是简洁太多了?
cpp
class ST
{
typedef int DataType;
private:
DataType* _a;
int _top;
int _capacity;
public:
void Init()
{
_a = NULL;
_top = _capacity = 0;
}
void Destory()
{
free(_a);
_top = _capacity = 0;
}
void Push(DataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
DataType* tmp = (DataType*)realloc(_a, newcapacity * sizeof(DataType));
if (tmp == NULL)
{
perror("realoc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top ] = x;
_top++;
}
void Pop()
{
assert(_top > 0);
--_top;
}
bool Empty()
{
return _top == 0;
}
int Top()
{
assert(_top > 0);
return _a[_top - 1];
}
};
你看,这就是面向对象的优势!我们来对比一下两者的区别:
|-------|-------------------|-------------------|
| 特性 | C 语言实现 | C++ 类实现 |
| 数据与函数 | 分离,函数要传结构体指针 | 封装在一起,不用传指针,直接调用 |
| 访问控制 | 没有,外部可以随便改结构体成员 | 有,私有成员外部不能改,数据更安全 |
| 语法便捷性 | 要写 typedef,函数要传地址 | 类名直接当类型,调用方法直接用. |
| 封装性 | 无,数据暴露在外 | 强,隐藏内部细节,只暴露接口 |
是不是一目了然?用 C++ 的类,代码更简洁,更安全,更好维护,这就是为什么我们要学面向对象的原因。
总结
今天我们学习了面向对象的入门基础:类的定义、访问限定符、类域,还有实例化、this 指针,最后我们用栈的例子,直观感受了面向对象比面向过程的优势。
这些都是类和对象的基础,一定要把例子自己敲一遍,动手实操比看十遍都有用!如果有任何不懂的地方,欢迎在评论区留言,我会一一回复!下一篇我们会讲构造函数和析构函数,带你进一步了解对象的初始化,别忘了点赞收藏关注,我是小小风,期待与你的下一次相遇!