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