
01_C++ 基础篇
本文最后更新于 2025-01-16,学习久了要注意休息哟
第一章 初探C++
1.1 C++ 介绍
1.1.1 C++的面向对象
面向对象编程(OOP)是一种程序设计范式,其核心思想是将现实世界的事物和概念抽象为“对象”,并通过这些对象的交互来实现程序功能。
在面向对象编程中,我们把实现某些事物的行为和属性封装成一个整体,这个整体通常就是类。当我们需要使用这个类时,根本不需要关心它的内部是如何实现的,只要知道怎么使用它就行了。
1、类的例子
就像你去餐馆点餐,你不需要关心厨师是怎么做菜的,只需要知道菜单上有你喜欢的菜,并选择你想要的。
- 属性:就像餐馆里的材料,比如胡萝卜、辣椒、肉,这些是餐馆拥有的资源(也就是类的成员变量)。
- 方法:做菜的方法,比如炒菜、煮汤,这些是餐馆的服务(也就是类中的函数)。
2、面向过程与面向对象
- 面向过程:你得自己从头到尾准备饭菜,买菜、洗菜、切菜、炒菜等,每一步都需要你亲自操作。
- 面向对象:你去餐馆,只需要点个菜,厨师帮你处理好一切。你不需要关心过程,餐馆已经把这些操作封装起来了。
4、面向对象的优势
-
低耦合性:不同部分之间关联较少,修改或更换其中一部分时,不会影响其他部分。 ==高内聚 低耦合==
-
易用性:使用者不需要关心类内部是如何实现的,只要知道如何调用这个类,就能完成工作。对于大型项目的开发,能大大提高效率。
通过面向对象的这种方式,我们能够把复杂的操作简化,专注于我们需要的结果,而不必担心实现的细节。
1.1.2 C和C++的区别
C 和 C++ 就像是两兄弟,虽然关系密切,但性格不同,解决问题的方式也不一样。
1、思维方式
- C语言 是个务实的家伙,做事情一板一眼。他喜欢面向过程,强调一步步解决问题。就像自己做饭,你得亲自买菜、切菜、炒菜。
- C++ 则更聪明,支持面向对象。他会把复杂的过程打包起来,你只需要点菜,背后的细节已经替你处理好了。
2、任务处理
- C语言 擅长处理简单、直接的任务,适合系统级编程。
- C++ 更擅长复杂任务,通过对象组合和复用,适合开发大型软件项目。
3、工作方式
- C语言 公开所有操作步骤,你得逐步实现每个细节。
- C++ 封装了细节,你只需要使用已经准备好的功能。
4、工具箱
- C语言 的工具简单实用。
- C++ 有一个强大的标准库(STL),提供更多的工具,帮助处理复杂的任务。
5、严格性
- C语言 更随意一些,对类型检查没那么严格。
- C++ 对代码要求更严格,能避免很多低级错误。
1.1.3 C++的应用领域
游戏(Cocos2d-X)、图像、多媒体、网络、嵌入式
数据库(Oracle、MySQL)、浏览器(Chrome)、搜索引擎(Google)
操作系统、驱动程序、编译器(GCC、LLVM)、编程语言(Swift)
HPC(High Performance Computing,高性能计算)
iOS开发(Runtime、AsyncDisplayKit)
Android开发(NDK、fresco【匿名共享内存,Ashmem,Anonymous Shared Memory】)
Java开发(JNI)
......
总结
C++之所以应用范围如此广泛,得益于它的高效性、稳定性、跨平台性
虽然C++在很多大型应用中,无法施展拳脚;但在某些领域,如同巨人一般而且是不可或缺的顶梁柱
基本只要是用到C++的地方,都是高大上的地方
不用c++写.就意味着大量浪费cpu.就意味着..你的作品不是又卡又慢.
工业
视觉 c++ c#
1s 10 -15
算法
MATLAB -> 生成 重写 C++ / c
1.2 第一个C++程序
输出
#include <iostream>
using namespace std;
int main_1() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
输入
// 导入头文件
#include "iostream"
// 命名空间
using namespace std;
// main 函数开始
int main()
{
// 创建变量
char name[128];
char ch = 'A';
// 输入
cin >> name;
// 输出
cout << name << endl;
// 类型转换
cout << static_cast<int>(ch) << endl;
// 返回
return 0;
}
1.2.2 准备工作
(1) C++ 后缀名
源文件
.cpp // 最常用
.C // 大写
.cxx
.cc
(2) C++ 头文件
C++中的标准头文件不需要加.h
,使用方式如下:
#include <iostream>
此外,C++兼容C语言的头文件,但写法不同。不是使用 #include <stdio.h>
这种形式,而是写成如下方式:
#include <cstdio>
#Include <stdio.h> --- > cstdio cstring
(3) C++ 返回值
在 C++ 中,main
函数的返回值必须是 int
类型,否则会报错,并且 main
函数必须返回一个值。
(4) C++ 编译方式
linux中编译方式
C++中通常使用 g++ 编译器,编译命令如下:
g++ xxx.cpp -o xx.out
如果使用 gcc 编译器编译 C++ 程序,则需要手动连接 C++ 的库,示例如下:
gcc xxx.cpp -lstdc++
2.1.2 输入输出
1、输入输出流
C++ 中并没有定义任何内置的输入输出语句,而是通过标准库提供 I/O机制。这个库就是我们常用的 iostream
库,iostream
库中定义了两个基础类型:istream
和 ostream
,分别代表输入流和输出流。
- 流:流是一种数据传输方式,它代表一个字符序列,从输入设备读取或者写入到输出设备中。流的意思是,随着时间的推移,字符是按顺序生成或者消耗的。
标准库中定义了四个常用的流对象:
cout // 标准输出流(输出到屏幕) Ostream
cin // 标准输入流(从键盘输入) Istream
cerr // 标准错误流(输出错误信息) Ostream
clog // 标准日志流(输出运行信息) Ostream
2、使用示例
示例程序:
int val_1;
int val_2;
std::cout << "请输入你要计算的两个数值:" << std::endl;
std::cin >> val_1 >> val_2;
std::cout << "val_1 + val_2 = " << val_1 + val_2 << std::endl;
3、输出示例
std::cout << "请输入你要计算的两个数值:" << std::endl;
这里我们使用了 C++ 中的输出运算符(<<
)。<<
运算符的左侧是 ostream
对象(即 std::cout
),右侧是要输出的值。我们可以连续使用 <<
运算符来输出多个值。
例如,以下表达式:
std::cout << "请输入你要计算的两个数值:" << std::endl;
等价于:
(std::cout << "请输入你要计算的两个数值:") << std::endl;
我们也可以将其拆分为两行:
std::cout << "请输入你要计算的两个数值:";
std::cout << std::endl;
endl
是一个特殊符号,表示换行并刷新输出缓冲区,使缓冲区中的数据真正输出到屏幕。
4、输入示例
std::cin >> val_1 >> val_2;
这里我们使用了 C++ 中的输入运算符(>>
)。和输出运算符类似,>>
的左侧是 istream
对象(即 std::cin
),右侧是接收输入值的变量。我们也可以连续使用 >>
运算符来读取多个值。
1.2.1 命名空间
1、命名空间的使用
你可能已经注意到,我们的程序中使用的是 std::cout
和 std::cin
,而不是直接写 cout
和 cin
。这是因为 C++ 中的所有标准库内容都放在了一个叫 std
的命名空间里。命名空间的作用是为了避免不同代码之间的名字冲突。
为什么需要命名空间?
想象一下,如果有两个人都用“张三”这个名字,大家会混淆他们是谁。命名空间就像在名字前面加上姓氏一样,帮助区分不同的功能。例如,std::cout
表示标准库中的 cout
,而 cout
可能在其他地方有不同的定义。
如何简化命名空间的使用?
如果你觉得每次都写 std::
太麻烦,可以使用下面两种方法:
- 使用
using
声明
在程序开始的地方加上:
using namespace std;
这样,你就可以直接写 cout
、cin
,而不用每次都加 std::
了。例如:
#include <iostream>
using namespace std;
int main() {
cout << "请输入两个数:" << endl;
int a, b;
cin >> a >> b;
cout << "a + b = " << a + b << endl;
return 0;
}
- 引入特定的对象
如果你只想简化某些部分的代码,也可以只引入需要的部分:
using std::cout;
using std::cin;
using std::endl;
这样你就只需要给这些部分去掉 std::
,其他的内容不受影响。
2、命名空间的编写
在 C++ 中,我们可以自己定义命名空间,将相关的变量、函数等封装到一个命名空间里,避免与其他部分的代码产生冲突。定义命名空间的基本格式如下:
namespace 命名空间名{
数据类型1 变量1;
数据类型2 变量2;
...
数据类型n 变量n;
}
下面通过三个例子来展示在不同情况下如何使用命名空间:
情况一:两个命名空间内变量名重复
当两个命名空间中定义了同名的变量时,直接使用变量名会产生歧义。为了解决这个问题,我们需要通过命名空间名来区分变量。
#include <iostream>
#include <cstring>
using namespace std;
// 定义两个命名空间
namespace test_1 {
char name[20];
int age;
}
namespace test_2 {
char name[20];
int age;
}
// 使用两个命名空间
using namespace test_2;
using namespace test_1;
int main()
{
// 由于两个命名空间的变量名重复,不能直接使用变量名,否则会产生歧义。
// 需要使用“命名空间名::变量名”的方式来访问。
test_1::age = 18;
test_2::age = 30;
cout << "test_1::age = " << test_1::age << endl;
cout << "test_2::age = " << test_2::age << endl;
return 0;
}
情况二:命名空间与全局变量冲突
当命名空间中的变量与全局变量发生冲突时,通常使用作用域运算符 ::
来访问全局变量,避免歧义。
这种方式我们称为 匿名空间 或者 域调用
#include <iostream>
#include <cstring>
using namespace std;
// 定义两个命名空间
namespace test_1 {
char name[20];
int age;
}
namespace test_2 {
char name[20];
int age;
}
using namespace test_1;
using namespace test_2;
// 全局变量
int age;
int main()
{
test_1::age = 18;
test_2::age = 30;
// 使用 "::age" 访问全局变量,避免与命名空间中的变量冲突 <匿名空间> <域调用>
::age = 50;
cout << "test_1::age = " << test_1::age << endl;
cout << "test_2::age = " << test_2::age << endl;
cout << "Global age = " << ::age << endl;
return 0;
}
情况三:命名空间与局部变量冲突
当命名空间中的变量与局部变量发生冲突时,程序会优先使用局部变量。此时,可以通过命名空间名来明确使用命名空间中的变量。
#include <iostream>
#include <cstring>
using namespace std;
// 定义两个命名空间
namespace test_1 {
char name[20];
int age;
}
namespace test_2 {
char name[20];
int age;
}
// 使用两个命名空间
using namespace test_2;
using namespace test_1;
int main()
{
test_1::age = 18;
test_2::age = 30;
// 局部变量
int age;
age = 50; // 优先使用局部变量
cout << "test_1::age = " << test_1::age << endl;
cout << "test_2::age = " << test_2::age << endl;
cout << "Local age = " << age << endl;
return 0;
}
3、命名空间封装函数
在 C++ 中,命名空间不仅可以封装变量,还可以封装函数。这种方式可以将不同功能模块分开,避免命名冲突。通过命名空间封装函数,我们可以轻松管理同名的函数、变量,使代码结构更加清晰。
下面通过一个示例展示如何将函数封装在命名空间中,并解释不同情况下的使用方法。
示例代码:
#include <iostream>
#include <cstring>
using namespace std;
// 命名空间问题
namespace test_1 {
char name[20];
int age;
int my_add(int x, int y); // 函数声明
}
namespace test_2 {
char name[20];
int age;
int my_add(int x, int y); // 函数声明
}
// 函数定义
int test_1::my_add(int x, int y)
{
cout << "test_1::my_add: " << x + y << endl;
return x + y;
}
int test_2::my_add(int x, int y)
{
cout << "test_2::my_add: " << x + y << endl;
return x + y;
}
int main()
{
// 访问命名空间中的变量
test_1::age = 18;
test_2::age = 30;
// 局部变量
int age;
age = 50; // 局部变量优先使用
// 调用命名空间中的函数
int result1 = test_1::my_add(5, 10); // 调用 test_1 的函数
int result2 = test_2::my_add(7, 20); // 调用 test_2 的函数
cout << "局部变量 age = " << age << endl;
cout << "test_1::age = " << test_1::age << endl;
cout << "test_2::age = " << test_2::age << endl;
return 0;
}
4、命名空间嵌套
在 C++ 中,命名空间不仅可以独立存在,还可以嵌套在其他命名空间中。嵌套命名空间的作用是将更细粒度的功能模块分组管理。通过命名空间的嵌套,我们可以进一步组织代码,避免命名冲突。
#include <iostream>
#include <cstring>
using namespace std;
// 命名空间问题
namespace group {
int value;
namespace zhangsan {
int value;
}
namespace lisi {
int value;
}
}
using namespace group;
int main()
{
// 访问问题
lisi::value = 60;
zhangsan::value = 50;
return 0;
}
1.3 C++ 中的字符串
在 C++ 中,字符串类型实际上是通过字符串类(string
类)来操作的。这种类方式的操作使得字符串处理更加方便。这里我们对字符串类进行一个简单的介绍,后续学习容器时还会深入了解字符串作为容器的更多功能。
1.3.1 赋值操作
string s1 = "Hello"; // 直接赋值
string s2;
s2 = "World"; // 使用赋值运算符
string s3(s1); // 拷贝构造
string s4(5, 'A'); // 重复字符赋值,s4 = "AAAAA"
2.2.2 赋值操作
C++ 的 string
类支持多种赋值方式:
string s1 = "Hello"; // 直接赋值
string s2;
s2 = "World"; // 使用赋值运算符
string s3(s1); // 拷贝构造
string s4(5, 'A'); // 重复字符赋值,s4 = "AAAAA"
2.2.3 字符串拼接
使用 +
运算符或 append()
函数进行字符串拼接:
string s1 = "Hello, ";
string s2 = "World!";
string s3 = s1 + s2; // 使用 + 进行拼接
s1.append(" C++"); // 使用 append 函数拼接
2.2.4 查找和替换
string
提供了 find()
和 replace()
函数用于查找和替换子字符串:
string s = "Hello, World!";
size_t pos = s.find("World"); // 查找子字符串 "World"
if (pos != string::npos) {
s.replace(pos, 5, "C++"); // 替换为 "C++"
}
2.2.5 字符串比较
C++ 中可以直接使用关系运算符(如 ==
, !=
, <
, >
等)来比较 string
对象:
string s1 = "apple";
string s2 = "banana";
if (s1 == s2) {
cout << "The strings are equal" << endl;
} else if (s1 < s2) {
cout << "s1 comes before s2" << endl;
}
2.2.6 字符串存取
可以使用下标操作符 []
或 at()
方法来访问和修改字符串中的字符:
string s = "Hello";
char c = s[1]; // 访问第二个字符 'e'
char c2 = s.at(2); // 使用 at() 方法访问第三个字符 'l'
注意:
[]
不会检查越界,而at()
会进行越界检查。
2.2.7 字符串插入
使用 insert()
函数可以在指定位置插入子字符串:
string s = "Hello!";
s.insert(5, " C++"); // 在索引5的位置插入 " C++",结果为 "Hello C++!"
2.2.8 字符串删除
string
类提供 erase()
函数来删除字符串中的部分内容:
string s = "Hello, C++!";
s.erase(5, 7); // 从索引5开始,删除7个字符,结果为 "Hello"
2.2.9 字符串清空
可以使用 clear()
函数来清空字符串的内容:
string s = "Hello!";
s.clear(); // 清空字符串,s 变为空字符串 ""
2.2.9 风格转换
1、C++ 字符串转 C 风格字符串
使用 string
类的 .c_str()
成员函数可以将 C++ 字符串转换为 C 风格的字符串(const char*
)。
#include <iostream>
#include <string>
using namespace std;
int main() {
string cppStr = "Hello, C++";
const char* cStr = cppStr.c_str(); // 转换为 C 风格字符串
printf("%s\n", cStr); // 使用 C 风格函数
return 0;
}
2、C 风格字符串转 C++ 字符串
C 风格的字符串(字符数组)可以直接赋值给 C++ 的 string
,C++ 会自动进行转换。
#include <iostream>
#include <string>
using namespace std;
int main() {
const char* cStr = "Hello, C";
string cppStr = cStr; // 自动转换为 C++ 字符串
cout << cppStr << endl;
return 0;
}
总结
- C++ 转 C:使用
.c_str()
。 - C 转 C++:直接赋值即可。
1.4 C++ 中的结构体
C++中 结构体和类的 唯一的区别,就是默认的访问控制权限不同:
- 类中成员的默认访问权限:private
- 结构体中成员的默认访问权限:public
==面试题==
1、C++中 类 和 结构体的区别
默认访问权限不同
2、C++中 什么时候使用 类 什么时候使用结构体
一般情况下,定义数据节点的时候,都使用结构体,如链表的节点。而逻辑操作比较多的时候,一般都是用类。如果分不清,就都是用 class 。
在 C++ 中,结构体与类几乎没有区别(除了默认的访问权限外,结构体默认是 public
的),可以包含数据成员和成员函数。这使得 C++ 的结构体功能大大增强,能够支持面向对象编程。
1.5 C++ 中的 bool 类型
bool
类型是 C++ 中用于表示布尔值的数据类型,表示真或假。
2.4.1 取值
true
:表示真,数值为 1。false
:表示假,数值为 0。
2.4.2 使用示例
#include <iostream>
using namespace std;
int main() {
bool isTrue = true; // 布尔变量赋值为 true
bool isFalse = false; // 布尔变量赋值为 false
cout << isTrue << endl; // 输出 1
cout << isFalse << endl; // 输出 0
return 0;
}
2.4.3 常用场景
- 用于条件判断,如
if
、while
等语句。 - 逻辑运算,如
&&
(与)、||
(或)、!
(非)。
第二章 核心要素
2.1 动态内存
在C++中支持 malloc / free 但是我们C++中一般使用 new delete
3.1.1 分配内存 new
单个空间的分配
格式: 数据类型 * 指针名 = new 数据类型
连续空间分配
格式:数据类型 * 指针名 = new 数据类型[数量]
如: int *p = new int[5]
3.1.2 回收内存 delete
回收单个空间
delete 指针名
连续的空间回收
delete []指针名
在括号内什么都不要写 表示释放从指针指向的空间开始连续的空间
3.1.3 示例
示例1:单个空间的分配
int* func()
{
int* a = new int(10);
return a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
//利用delete释放堆区数据
delete p;
//cout << *p << endl; //报错,释放的空间不可访问
return 0;
}
示例2:连续空间分配
//堆区开辟数组
int main() {
int* arr = new int[10];
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100;
}
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
//释放数组 delete 后加 []
delete[] arr;
return 0;
}
3.1.4 new/delete 和 malloc/free 的区别
要记,面试会问
1、new/delete
是 C++ 的关键字,malloc/free
是库函数。
2、new
在分配空间的同时可以进行初始化,malloc
不可以。
3、new
分配的空间如果没有初始化,默认会初始化成 0;而 malloc
分配的空间如果没有初始化,里面都是随机值,需要自己手动调用 memset
或者 bzero
函数来清 0。
4、new
是根据类型分配的空间,而 malloc
是根据字节数分配空间的,需要自己调用 sizeof()
计算。
5、new
是要什么类型,返回的就是什么类型的指针;而 malloc
返回的都是 void*
,需要自己强转成需要的类型。
6、new
在分配类对象空间的时候,会调用类的==构造函数==,malloc
不会。
7、delete
在释放类对象空间的时候,会调用类的==析构函数==,free
不会。
2.2 引用
3.2.1 引用的概念
引用是 C++ 对 C 语言的重要扩展。
它的作用是为变量创建一个别名,使得通过这个别名可以直接操作原变量。引用的概念类似于 Linux 系统中的硬链接。任何对引用的操作,实际上都是对原变量的操作。
示例:
int a = 10;
int &r = a; // r 是 a 的引用
r = 20; // 修改 r 的值会影响 a
3.2.2 引用的基本语法
引用使用 &
作为标识符。
基本格式:
数据类型 &引用名 = 引用的目标;
int a = 10;
int &r = a; // 定义一个 int 类型的引用 r,引用变量 a
3.2.3 引用的本质
引用的本质是 C++ 内部实现的 指针常量。因此,引用与指针有着相似之处,但它是更高层次的抽象,提供了更方便的操作方式。
定义引用时需要注意以下几点:
引用必须初始化:定义引用时,必须为其指定一个初始化目标。
int a = 10;
int &r = a; // r 必须在定义时初始化
引用的类型与目标类型必须一致:引用的类型必须与它所引用的变量类型保持一致。(继承和多态除外)
int a = 10;
int &r = a; // 类型一致
引用不可更改目标:一旦引用绑定到一个变量上,不能再更改其引用的目标。
int a = 10;
int &r = a;
r = 20; // 修改 r 的值,a 的值也会变为 20
3.2.4 引用作为函数参数
**作用:**函数传参时,可以利用引用的技术让形参修饰实参
**优点:**可以简化指针修改实参
示例
#include "iostream"
using namespace std;
// 值传递
void func_swap_01(int val_1 , int val_2){
int temp = val_1;
val_1 = val_2;
val_2 = temp;
}
// 值传递
void func_swap_02(int *val_1 , int *val_2){
int temp = *val_1;
*val_1 = *val_2;
*val_2 = temp;
}
// 值传递
void func_swap_03(int &val_1 , int &val_2){
int temp = val_1;
val_1 = val_2;
val_2 = temp;
}
int main()
{
int val_1 = 10;
int val_2 = 20;
func_swap_01(val_1, val_2);
cout << "val_1 = " << val_1 << " val_2 = " << val_2 << endl;
func_swap_01(val_1, val_2);
cout << "val_1 = " << val_1 << " val_2 = " << val_2 << endl;
func_swap_01(val_1, val_2);
cout << "val_1 = " << val_1 << " val_2 = " << val_2 << endl;
}
结果
val_1 = 10 val_2 = 20
val_1 = 10 val_2 = 20
val_1 = 10 val_2 = 20
3.2.5 引用与指针
用法如下
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
typedef struct Node {
int data;
struct Node* next;
}Node;
void create_node(Node * &p)
{
p = (Node*)malloc(sizeof(Node));
p->data = 100;
p->next = nullptr;
}
int main()
{
Node* list = nullptr;
create_node(list);
cout << list->data << endl;
}
3.2.6 引用与返回值
我们平时使用函数时,函数的返回值都是一个右值。
但是引用作为函数的返回值时,返回值是一个左值。
- 左值:既可以放在等号左边,也可以放在等号右边的值。
- 右值:只能放在等号右边的值。
引用作为函数的返回值,不能返回局部变量的引用,因为局部变量在函数调用结束时就被回收了。
可以返回全局变量的引用或者 static
修饰的局部变量的引用。
示例1
#include <iostream>
using namespace std;
// 返回静态局部变量的引用
int &my_add(const int &x, const int &y) {
static int temp = x + y; // 静态局部变量
return temp; // 返回 temp 的引用
}
// 返回全局变量 value 的引用
int value = 100;
int &func() {
return value; // 返回 value 的引用
}
int main() {
// 返回 temp 的引用
int &r1 = my_add(10, 20); // r1 绑定到 temp
cout << r1 << endl; // 输出 30
// 返回全局变量 value 的引用
int &r2 = func(); // r2 绑定到 value
cout << r2 << endl; // 输出 100
// 修改 value 的值
r2 = 200;
cout << value << endl; // 输出 200
// 通过 func() 修改 value
func() = 300;
cout << value << endl; // 输出 300
return 0;
}
示例2
#include <iostream>
int& getElement(int arr[], int index) {
return arr[index]; // 返回数组元素的引用
}
int main() {
int numbers[] = {10, 20, 30, 40, 50};
// 将getElement返回的引用作为左值
getElement(numbers, 2) = 100; // 修改索引为2的元素
// 输出修改后的数组
for (int i = 0; i < 5; ++i) {
std::cout << numbers[i] << " ";
}
std::cout << std::endl;
return 0;
}
3.2.7 引用与结构体
当结构体中包含引用时,定义结构体的对象时,必须对引用进行初始化。由于引用必须在创建时绑定到某个变量,因此不能延迟初始化。
1、引用结构:
2、结构体内部有引用:
初始化方式
MyStruct T1 = { .valur = 20 , .ref = val }; // 错误的写法,不能使用初始化列表的指定成员
MyStruct T1 = { 20 , val }; // 正确的写法,必须按照结构体成员顺序进行初始化
代码示例
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
struct MyStruct {
int valur; // 普通成员
int &ref; // 引用成员,必须在初始化时绑定
};
int main() {
int val = 10; // 用于初始化引用的变量
MyStruct T1 = { 20, val }; // 初始化结构体,包括引用成员
val = 20; // 修改引用绑定的变量的值
cout << T1.ref << endl; // 输出 20,因为引用指向 val
}
3.2.8 引用与常量
当 const
修饰引用时,它被称为常引用。常引用的作用是禁止通过引用来修改其引用的目标变量。
作用:防止对引用目标的错误修改,保护数据的完整性。
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
int func_Add(const int &val_1, const int &val_2) {
return val_1 + val_2;
}
int main() {
// 变量
int val = 10;
// 引用 常量引用
cosnt int &ref = val;
// 常量引用 引用变量
cout << "ref = " << ref << endl;
// 常量引用 作为参数
int val_1 = 80;
int val_2 = 80;
cout << "val_1 + val_2 = " << func_Add(val_1, val_2) << endl;
// 常用引用 引用常量
// int & ref_1 = 10; // 不使用常量引用是不能引用常量的
const int & ref_1 = 10;
const int val_3 = 50;
// int & ref_2 = val_3; // 不使用常量引用是不能引用const 所修饰的常量
const int & ref_2 = val_3;
// 常量引用 引用临时值
// int & ref_3 = val_1 + val_2; // 不使用常量引用是不能引用临时值的
const int & ref_3 = val_1 + val_2;
}
3.2.9 引用和指针的区别
要记,面试会问
1、引用必须初始化,指针可以不初始化。
2、引用不可以改变指向,指针可以。
3、不存在指向 NULL 的引用,指针可以指向 NULL。
4、指针在使用前需要检查合法性,引用不需要。
2.3 函数
2.3.1 默认参数
在 C 语言 中,函数的参数必须由实参传递。而在 C++ 中,允许给函数的参数提供默认值。
默认值的规则
- 调用函数时,传递了实参,就使用实参的值。
- 没有传递实参时,使用默认值。
注意事项
- 默认参数必须遵循靠右原则,即从左往右连续的参数可以有默认值,靠左的参数不能有默认值,否则会引发歧义。
- 默认参数只能在声明处提供,不能同时在定义处重复默认参数。
示例代码
#include <iostream>
using namespace std;
// 函数定义带有默认参数
int func(int a, int b = 10, int c = 10) {
return a + b + c;
}
// 函数声明带有默认参数,定义时不再写默认参数
int func2(int a = 10, int b = 10);
int func2(int a, int b) {
return a + b;
}
int main() {
cout << "ret = " << func(20, 20) << endl; // 使用实参 20 和 20,输出 50
cout << "ret = " << func(100) << endl; // 只传递 100,使用默认参数,输出 120
system("pause");
return 0;
}
输出结果:
ret = 50
ret = 120
2.3.2 函数哑元
占位参数
占位参数是在函数形参列表中出现,但没有具体使用的参数。它在某些情况下可以作为一种占位符,使函数的调用更加灵活。
- 在定义函数时,可以为某个或多个参数指定类型,但不需要给出参数名。
- 占位参数仅用于占位,没有其他实际意义,但调用时必须填补占位参数。
语法
返回值类型 函数名 (数据类型) {}
在现阶段,函数的占位参数的意义可能不明显,但在更复杂的应用中会有使用场景。
示例
#include <iostream>
using namespace std;
// 函数带占位参数,占位参数没有使用
void func(int a, int) {
cout << "this is func" << endl;
}
int main() {
func(10, 10); // 占位参数必须传递值
return 0;
}
输出
this is func
占位参数在一些特殊场景中很有用,后面的课程可能会进一步涉及这类技术。
使用场景
- 当我们进行自增、自减运算符 重载 会用到哑元进行占位置
2.3.3 函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
这些函数接受的形参类型不一样,但是执行的操作非常类似。当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数。
这些函数接受的形参类型不一样,但是执行的操作非常类似。当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数
1、函数重载的概念
当我们在封装功能相同、用法相同的函数时,只是因为参数的不同,就需要定义多个版本的函数。为了避免函数名重复导致的不便,C++ 提供了函数重载功能。
函数重载:允许定义多个同名的函数,但参数列表必须不同。
作用:函数名可以相同,提高代码的复用性,简化调用。
2、函数重载的要求
1、同一个作用域下
2、函数名相同,形参列表必须不同。
-
类型不同
-
个数不同
-
顺序不同
3、函数重载对返回值没有要求。
注意: 函数的返回值不可以作为函数重载的条件。
3、函数重载的示例
#include <iostream>
using namespace std;
// 重载函数,根据参数的类型和个数来区分
void print(int i) {
cout << "整数: " << i << endl;
}
void print(double d) {
cout << "浮点数: " << d << endl;
}
void print(int i, double d) {
cout << "整数: " << i << " 浮点数: " << d << endl;
}
int main() {
print(10); // 调用第一个重载函数
print(3.14); // 调用第二个重载函数
print(10, 3.14); // 调用第三个重载函数
return 0;
}
4、函数重载的原理
在调用函数时,编译器会根据实参的类型和个数自动选择匹配的重载函数。
编译器原理:编译器在编译过程中,会根据每个函数的参数类型和个数生成唯一的函数名称。这一过程称为名字改编(Name Mangling)。虽然源代码中的函数名相同,但在底层,编译器通过名字改编机制确保每个重载函数都有不同的符号名称。
通过查看汇编代码的方式理解原理
可以通过生成汇编代码来查看重载函数的原理。使用如下步骤:
gcc -E xxx.cpp -o xxx.i # 预处理阶段,生成预处理文件
gcc -S xxx.i -o xxx.s # 生成汇编代码文件
在生成的汇编文件中,可以通过搜索关键字 globl
来获取重载函数在汇编代码中的名字。例如:
_Z6printi // 对应 print(int)
_Z6printd // 对应 print(double)
_Z6printid // 对应 print(int, double
这些经过名字改编后的符号表示编译器根据参数类型生成了唯一的函数名,确保不同重载函数在底层不会冲突。
5、函数重载的注意事项
引用作为重载条件
在 C++ 中,引用可以作为函数重载的条件。通过区分左值引用和 const
引用,编译器可以根据传递的参数是左值还是右值来选择合适的函数。
#include <iostream>
using namespace std;
void func(int &a) {
cout << "func (int &a) 调用 " << endl; // 左值引用
}
void func(const int &a) {
cout << "func (const int &a) 调用 " << endl; // 常量引用
}
int main() {
int a = 10;
func(a); // 调用非 const 左值引用的版本
func(10); // 调用 const 引用的版本(右值可以绑定到 const 引用)
return 0;
}
函数重载碰到函数默认参数
当函数重载遇到默认参数时,可能会产生二义性。编译器在调用函数时可能无法决定应该调用带默认参数的重载版本,还是另一个参数匹配的重载版本,因此会报错。
#include <iostream>
using namespace std;
void func2(int a, int b = 10) {
cout << "func2(int a, int b = 10) 调用" << endl; // 带默认参数的函数
}
void func2(int a) {
cout << "func2(int a) 调用" << endl; // 无默认参数的函数
}
int main() {
// func2(10); // 这行代码会产生二义性
func2(10, 20); // 直接传递两个参数,明确调用两个参数的版本
return 0;
}
2.3.4 内联函数
内联函数通过直接展开代码,减少函数调用的跳转时间,提高执行速度,但可能会让程序变大。
定义方法
在函数前加上 inline
关键字。
要求
- 内联函数必须写在头文件(.h)。
- 函数体要尽量简短,太大的函数不适合做内联。
具体是否调用处展开,由编译器决定,我们决定不了
例子
// func.h
inline int my_add(int a, int b) {
return a + b;
}
// main.cpp
#include <iostream>
#include "func.h"
int main() {
int a = my_add(20, 30); // 调用内联函数
std::cout << a << std::endl; // 输出50
return 0;
}
内联函数和宏的区别
<STM32面试有可能遇到>
1、宏定义是在预处理阶段完成替换,内联函数是在编译阶段处理的
2、内联函数本质也是函数,有函数的属性,也对函数的类型做检查,而宏定义只是简单的无脑替换
示例
// 内联函数
inline int my_min1(int x, int y) {
return x < y ? x : y;
}
// 带参数的宏定义
#define MY_MIN2(x, y) x<y?x:y
int main() {
int a = 1;
int b = 3;
int c = 0;
c = my_min1(++a, b);
cout << "a = " << a << " b = " << b << " c = " << c << endl; // 输出:2 3 2
a = 1;
b = 3;
c = 0;
c = MY_MIN2(++a, b);
cout << "a = " << a << " b = " << b << " c = " << c << endl; // 输出:2 3 1
return 0;
}
宏定义:
- 预处理阶段:宏定义是在预处理阶段完成文本替换。它并不是函数,编译器不会检查参数类型,只是简单的文本替换。
- 潜在问题:宏定义中的参数会被多次替换,可能导致副作用,如
++a
在宏中会执行两次。
内联函数:
- 编译阶段:内联函数是在编译阶段处理的,编译器会根据内联函数的调用展开代码,类似于宏,但仍然保留了函数的类型检查和语法规则。
- 优势:内联函数不会像宏那样出现参数重复执行的问题,它的参数只会计算一次。
inline int MY_mai_1(int x , int y){
return x < y ? x : y;
}
#define MY_mai_2(x , y) x < y ? x : y
示例分析
my_min1(++a, b) 调用了内联函数:
++a 只执行一次,a 从 1 变为 2,比较 2 < 3,结果为 true,返回 2。
输出:a = 2, b = 3, c = 2。
MY_MIN2(++a, b) 调用了宏定义:
宏定义替换后 ++a 执行两次,第一次 a 变为 2,比较 2 < 3,结果为 true,再次执行 ++a,a 变为 3,返回 3。
输出:a = 3, b = 3, c = 3。
第三章 面向对象-封装
3.1 面向对象
3.1.1 OOP思想
随着计算机的发展,大型程序越来越复杂,面向过程的思想在设计大型系统中有点捉襟见肘,进而产生了面向对象的设计思想,而面向对象具有代表型的语言就是C++。
面向对象的程序设计的思路和人们日常生活中处理问题的思路是相似的。在自然世界和社会生活中,一个复杂的事物总是由许多部分组成的。任何一个事物都可以看成一个对象(object)。对象可大可小,是构成系统的基本单位。
任何一个对象都应当具有这两个要素,即属性和行为,它能根据外界给的信息进行相应的操作。一个对象往往是由一组属性和一组行为构成的。一般来说,凡是具备属性和行为这两种要素的,都可以作为对象。
在设计类的时候,属性和行为写在一起,表现事物

3.1.2 抽象思想
面向对象编程中的抽象并不是将问题拆解为一系列的步骤,而是从问题或系统中抽取出关键的元素,并明确这些元素的属性和行为。通过这种方式,我们简化了问题,将其转化为更易于理解和操作的对象。
,抽象就是从复杂的事物中抽取出一个或多个对象,并描述它们的属性和行为,以此来减少复杂性。
更通俗的解释
抽象的过程就是从一堆杂乱的东西中,挑选出有意义的部分,并加以描述和处理。可以通过类(Class)来进行抽象,类定义了对象的属性(数据)和方法(行为)。
例如,在一个动物园系统中,抽象出“动物”这一概念,我们不需要关心具体的动物如何移动、如何吃饭,只需定义一个动物对象,它具有行为(如吃、动)和属性(如名字、种类),然后让不同的动物继承这个概念,并各自实现自己的行为。
关键点
- 抽象是通过识别系统或问题中的关键组成部分,忽略细节,专注于重要特征。
- 抽象的目标是简化复杂性,将复杂的现实世界转化为更简洁、可操作的类和对象。
3.1.3 UML类图
UML(统一建模语言,Unified Modeling Language)是一种标准的图形化建模语言,用于描述和设计系统的结构、行为及其交互。UML类图是UML的一部分,用于表示类的静态结构,包括类的属性、方法以及它们之间的关系。类图在面向对象设计中起到了非常重要的作用,帮助开发者清晰地理解系统的结构和类之间的联系。
1、 UML类图的基本元素
类(Class)
类是面向对象的基本单位,它定义了对象的属性和行为。类图中的类通常表示为一个矩形,矩形分为三个部分:
- 类名:在顶部,表示类的名称。
- 属性(成员变量):在中间,表示类的特征。
- 方法(成员函数):在底部,表示类的行为。
2、UML类图的修饰符
类的属性和方法前会有修饰符,表示它们的可见性:
+
:公有(Public),表示该成员可以被任何其他类访问。-
:私有(Private),表示该成员只能在类内部访问。#
:受保护(Protected),表示该成员只能在类内部及其派生类中访问。~
:包私有(Package),表示该成员只能在同一包内访问。

3.2 封装实现
3.2.1 封装 - 类
C++中对象的类型称为类(class),类代表了某一批对象的共性和特征,类是对象的抽象,而对象是类的具体实例(instance)。
先声明一个类类型,然后用它去定义若干个同类型的对象。对象就是类类型的一个变量。可以说类是对象的模板,是用来定义对象的一种抽象类型。
类是抽象的,不占用内存,而对象是具体的,占用存储空间。在一开始时弄清对象和类的关系是十分重要的。类是用户自己指定的类型。
如果程序中要用到类类型,必须自己根据需要进行声明,或者使用别人已设计好的类。C++标准本身并不提供现成的类的名称、结构和内容。
3.2.2 类的权限
class 类名{
private:
// 私有的成员变量(属性/数据) 和 成员函数(方法/行为)
protected:
// 受保护的成员变量(属性/数据) 和 成员函数(方法/行为)
public:
// 共有的成员变量(属性/数据) 和 成员函数(方法/行为)
};
访问控制权限
public: 公共: 修饰的成员 在类内 子类 类外 都可以直接访问
protected: 保护: 修饰的成员 在类内 子类 可以直接访问 类外不行
private: 私有: 修饰的成员 在内类 可以直接访问 子类 类外 不行
关于他们之间的关系如下表
类内 | 子类 | 类外 | |
---|---|---|---|
public | √ | √ | √ |
protected | √ | √ | × |
private | √ | × | × |
类的成员函数访问权限:
- 类的成员函数可以访问类的所有成员变量,包括
public
、protected
和private
。即使是通过函数传递过来的同类对象的私有成员,也可以访问。
访问控制是针对整个类:
- 访问控制是针对于整个类的,而不是某个具体的类对象。类内所有的成员函数和成员变量都受访问控制权限的影响。
访问控制的作用范围:
- 访问控制权限一旦声明,后面的所有成员都将遵循该权限,直到出现新的访问权限声明,或者类的定义结束。
访问控制声明的简洁性:
- 通常情况下,访问权限只需要在类内声明一次。多次声明是允许的,但不常见。
默认访问权限:
- 如果类中没有明确指定访问控制权限,默认情况下,所有成员都是
private
。
访问控制的目的:
- 访问控制的存在是为了保证类的正确使用。类的设计者通过设置访问权限,防止使用者以不合理的方式操作类的成员。例如,防止圆类的半径被赋值为负数等不合逻辑的操作。
3.2.3 类的函数
在 C++ 中,类的成员函数可以通过两种方式声明和定义:
类内声明,类外定义
在类的内部只进行函数的声明,而函数的定义在类的外部完成。
类内声明,类内定义
在类的内部同时进行函数的声明和定义。

类内声明,类外定义
#include <iostream>
using namespace std;
class MyClass {
public:
// 类内声明
void printHllo();
};
// 类外定义
void MyClass::printMessage() {
cout << "Hello" << endl;
}
int main() {
MyClass obj;
obj.printMessage(); // 调用类外定义的函数
return 0;
}
类内声明,类内定义
#include <iostream>
using namespace std;
class MyClass {
public:
// 类内声明和定义
void printHllo() {
cout << "Hello" << endl;
}
};
int main() {
MyClass obj;
obj.printMessage(); // 调用类内定义的函数
return 0;
}
- 类内声明,类外定义:通常用于保持类的定义简洁,便于维护较大的程序。
- 类内声明,类内定义:适合简短的函数,能让类的定义更紧凑。
3.3 构造与析构
当我们实例化一个类对象,有时需要对类对象内部的数据进行初始化,例如设定一个学生类,我们需要设置学生类中的姓名、年龄、学号等,再例如,成员变量中有引用或const修饰的变量,那么我们就必须进行一个赋初值的操作。
除了需要进行初始化操作,我们还需要对一些成员变量做销毁操作,例如有些数据需要做清0操作,有一些的还需要释放空间。
在C++中对这种操作定义了一类专门的函数,就是我们接下来要学习的构造函数和析构函数。
3.3.1 构造函数和析构函数
在进行程序编写并操作变量时,我们经常需要注意的两个安全问题:初始化、释放内存。
并且程序员在编写的过程中还非常的容易遗忘这两个问题,在C++ 中解决这个问题,利用了构造函数以及析构函数。
构造函数:用于实例化类对象的时候进行对成员属性进行初始化操作,构造函数由编译器自动调用,不能手动调用。
析构函数:用于类对象消亡前,执行对成员属性的清理工作,系统自动调用,当然也可以手动调用。
上述两种函数都是可以由编译器或系统自动调用,所以如果程序员没有编写构造和析构函数,那么编译器会自动提供一个空的构造和析构函数。
构造函数语法:类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法: ~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
class Student
{
public:
//构造函数
Student()
{
cout << "Person的构造函数调用" << endl;
}
//析构函数
~Student()
{
cout << "Person的析构函数调用" << endl;
}
};
int main() {
Student S1;
return 0;
}
3.3.2 构造函数的分类
两种分类方式:
按参数分为: 有参构造和无参构造
按类型分为: 普通构造和拷贝构造
三种调用方式:
括号法
显示法
隐式转换法
class Student {
public:
int age;
public:
//无参(默认)构造函数
Student() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Student(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
Student(const Person& p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~Student() {
cout << "析构函数!" << endl;
}
};
int main() {
//1 括号法,常用
Student s; //调用无参构造函数
//2 括号法,常用
Student s1(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2();
//3 显式法
Student s2 = Person(10);
Student s3 = Person(p2);
//Person(10)单独写就是匿名对象 当前行结束之后,马上析构
//4 隐式转换法
Student s4 = 10; // Person p4 = Person(10);
Student s5 = p4; // Person p5 = Person(p4);
//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
//Student p5(p4);
return 0;
}
3.3.3 构造函数的调用
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
class Student {
public:
Student() {
cout << "无参构造函数!" << endl;
mAge = 0;
}
Student(int age) {
cout << "有参构造函数!" << endl;
mAge = age;
}
Student(const Person& p) {
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
//析构函数在释放内存之前调用
~Student() {
cout << "析构函数!" << endl;
}
public:
int mAge;
};
//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
Student man(100); //p对象已经创建完毕
Student newman(man); //调用拷贝构造函数
Student newman2 = man; //拷贝构造
//Student newman3;
//newman3 = man; //不是调用拷贝构造函数,赋值操作
}
//2. 值传递的方式给函数参数传值
//相当于Student p1 = p;
void doWork(Student p1) {}
void test02() {
Student p; //无参构造函数
doWork(p);
}
//3. 以值方式返回局部对象
Person doWork2()
{
Student p1;
cout << (int *)&p1 << endl;
return p1;
}
void test03()
{
Student p = doWork2();
cout << (int *)&p << endl;
}
int main() {
//test01();
//test02();
test03();
return 0;
}
3.3.4 初始化列表
在定义构造函数的时候,可以使用冒号的方式 引出 构造函数的初始化列表
格式
类名(构造函数实参表):成员1(初值1),成员2(初值2)
{
构造函数函数体;
}
示例
#include "iostream"
using namespace std;
class Student{
public:
// 有参构造 重载
Student(string name , int age)
{
this->name = name;
this->age = new int(age);
}
// 无参构造 重载
Student():name("无名"),age(new int(0))
{
cout << "无参构造函数启动" << endl;
}
void show()
{
cout << "姓名 : " << this->name << endl;
cout << "性别 : " << * this->age << endl;
}
private:
string name;
int * age;
};
int main()
{
Student S1;
S1.show();
return 0;
}
结果
无参构造函数启动
姓名 : 无名
性别 : 0
==面试题==:必须使用初始化列表的场景
必须使用初始化列表的场景
场景1:当形参列表 和 成员变量 名字重名的情况下
可以使用初始化表 或者 this指针 解决
场景2:当 成员变量 中 有引用类型的 成员时
这个时候 必须使用 初始化列表
类名(type & num):val(num);
场景3: 当类中有const 修饰的成员变量时
场景4: 当类中有成员子对象时
类中 的 成员子对象 的有参构造
当类中有成员子对象时,需要在构造函数的初始化中调用成员子对象的构造函数
并传参完成对成员子对象初始化,如果没有调用成员子对象的构造函数
默认会调用成员子对象的无参构造函数 但是如果 成员子对象 没有无参构造 则会报错
3.3.5 拷贝构造函数
C++ 会为类生成一个默认拷贝构造函数和赋值操作符。这两个默认实现的行为都是逐位复制(即浅拷贝),它只是简单地复制每个成员的值,包括指针的地址,而不分配新的内存空间。
所以我们需要 自己写一个 深拷贝 构造函数来替换掉程序中的 浅拷贝构造函数。
格式
类名(const 类名 & other)
拷贝构造函数启动规则
类名 对象1
启动规则如下:
类名 对象2(对象1)
类名 对象2 = 对象1
类名 * 对象2 = new 类名(对象1)
浅拷贝
浅拷贝只是简单地将对象的值(包括指针的地址)赋值给新的对象。对于指针成员,浅拷贝只是复制指针本身,而不会为指针指向的动态内存分配新的空间。这意味着两个对象中的指针将指向同一块内存。
浅拷贝的风险
如果两个对象都指向同一块内存,当其中一个对象修改这块内存时,另一个对象也会受到影响。此外,当对象被销毁时,如果两个对象都尝试释放同一块内存,就会导致 双重释放(double deletion)错误,程序可能崩溃。
浅拷贝示例
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
// 有参构造函数
Student(string name, int age) {
this->name = name;
this->age = new int(age); // 动态分配内存
cout << "有参构造函数调用: " << name << endl;
}
// 展示信息
void show() const {
cout << "姓名: " << name << ", 年龄: " << *age << endl;
}
// 析构函数
~Student() {
delete age; // 释放动态分配的内存
cout << "析构函数调用: 释放 " << name << " 的动态内存" << endl;
}
string name;
int* age; // 使用指针来模拟动态内存
};
int main() {
Student S1("张三", 20);
Student S2("李四", 22);
// 浅拷贝:赋值操作,S1 的 age 指针指向 S2 的 age 所指向的内存
S1 = S2; // 调用了默认的赋值操作符(浅拷贝)
// 修改 S1 的年龄
*S1.age = 30;
cout << "修改 S1 的年龄后:" << endl;
S1.show(); // S1 的年龄变为 30
S2.show(); // S2 的年龄也变为 30,因为它们共享同一块内存
return 0;
}
深拷贝
是为指针成员分配新的内存空间,并复制原对象中的数据到新的内存。这保证了每个对象都有自己独立的一份数据,不会共享同一块内存,因此也不会引发双重释放的问题。
实例
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
// 有参构造函数
Student(string name, int age) {
this->name = name;
this->age = new int(age); // 动态分配内存
cout << "有参构造函数调用" << endl;
}
// 拷贝构造函数:深拷贝
Student(const Student& other) {
this->name = other.name; // 拷贝普通成员
this->age = new int(*other.age); // 深拷贝指针成员
cout << "深拷贝构造函数调用" << endl;
}
// 展示信息
void show() const {
cout << "姓名: " << name << ", 年龄: " << *age << endl;
}
// 析构函数
~Student() {
delete age; // 释放动态分配的内存
cout << "析构函数调用: 释放" << name << "的动态内存" << endl;
}
private:
string name;
int* age; // 使用指针来模拟动态内存
};
int main() {
Student S1("张三", 20);
Student S2 = S1; // 调用深拷贝构造函数
// 修改 S2 的年龄,不影响 S1
*S2.age = 30;
S1.show(); // S1 的年龄仍然是 20
S2.show(); // S2 的年龄是 30
return 0;
}
3.3.6 类对象作为类成员
==面试题==
深拷贝和浅拷贝的区别
浅拷贝:
如果类中没有显性的定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数,这个默认的拷贝构造函数,只完成成员之间的简单赋值。如果类中没有指针成员,只用这个默认的拷贝构造函数,是没有问题的。
深拷贝:
如果类中有指针成员,并且使用浅拷贝,指针成员之间也是只做了简单的赋值。相当于两个对象的指针成员指向的是同一块内存空间,调用析构函数的时候,就会出现 double free 的问题。此时, 需要在类中显性的定义拷贝构造函数,并且给新对象的指针成员分配空间,再将旧对象的指针成员指向的空间里的值拷贝一份过来。
==面试题==
在C++中,当你定义了一个空类时,编译器会默认生成哪些函数?请写出它们的声明格式。
class Test {}; // 定义了一个空类Test
// 编译器默认生成的函数如下:
// 1. 默认构造函数:
Test(void);
// 2. 拷贝构造函数:
Test(const Test &other);
// 3. 析构函数:
~Test(void);
// 4. 拷贝赋值运算符:
const Test &operator=(const Test & other );
3.4 this 指针
3.4.1 对象存储空间
在C++中,类的成员变量和成员函数的存储位置有所区别。具体规则如下:
- 非静态成员变量:属于类的每个对象。每个对象都包含自己的一份非静态成员变量的存储空间。
- 静态成员变量:不属于类的对象,而是属于类本身。所有对象共享同一个静态成员变量,静态成员变量在类的全局静态存储区中分配空间。
- 成员函数:无论是非静态成员函数还是静态成员函数,都是类的一部分,存储在代码段中,并不占用对象的存储空间。
静态成员变量 : 程序开始创建
静态成员函数 : 程序开始创建
静态 全局
普通成员函数 : 栈
非静态成员变量 : 栈
栈 对象本身
int id;
void func(int id)
示例程序
#include <iostream>
using namespace std;
class MyClass {
public:
// 非静态成员变量,占用对象的空间
int A;
// 静态成员变量,不占用对象的空间
static int B;
// 非静态成员函数,不占用对象的空间
void func() { }
// 静态成员函数,不占用对象的空间
static void sfunc() {}
};
int MyClass::B = 0; // 静态成员变量需要在类外进行定义
int main() {
cout << "对象的大小: " << sizeof(MyClass) << " 字节" << endl; // 输出类对象的大小
return 0;
}
存储说明
非静态成员变量(如 A): 每个对象都拥有自己的存储空间。
静态成员变量(如 B): 不随对象变化,所有对象共享同一份空间,因此对象的大小不受静态成员的影响。
成员函数: 无论是静态还是非静态成员函数,都不存储在对象中,而是由所有对象共享一个函数的实例。
3.4.2 this指针
首先我们要知道,类中的成员函数和静态成员变量并不存储在对象中,而是系统中仅有一份,所有对象通过它们进行调用。
那么,当调用成员函数时,系统如何知道要操作的变量是当前对象的呢?
这时就需要用到 this
指针,它指向调用该函数的具体对象,从而确保函数操作的是该对象的成员变量。
在 C++ 中,this
指针是一个隐含于类的每个非静态成员函数中的特殊指针。它指向调用该成员函数的对象本身。
并且,this
指针是不需要定义的 直接使用即可。
使用 this
指针的意义:
- 定位当前对象:当一个对象调用其成员函数时,成员函数可以通过
this
指针访问调用它的对象的成员变量。 - 非静态成员:由于非静态成员变量属于对象,每次调用成员函数时,编译器使用
this
指针来确保函数操作的是调用它的对象的成员,而不是其他对象的成员。
this
指针式类中成员函数的一个隐藏形参,那个对象用来调用成员函数 this
指针就指向谁
复习:常量 & 指针
const & 指针
const char * ptr; 不能修改数据 但是可以修改指向
char const * ptr; 不能修改数据 但是可以修改指向
char * const ptr; 不能修改指向 但是可以修改数据
所以 this
指针的类型: const * this
注意事项
1、不能在成员函数的形参中使用 this 指针
2、不能在构造函数的初始化列表中使用 this 指针
3、在成员函数的函数体中可以使用
示例程序
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
void set_data(string name , string sex, int age , int id );
void prt_data();
private:
string name;
string sex;
int age;
int id;
};
void Student::set_data(string name , string sex, int age , int id ) {
this->name = name;
this->sex = sex;
this->age = age;
this->id = id;
}
void Student::prt_data()
{
cout << "姓名:" << this->name << endl;
cout << "性别:" << this->sex << endl;
cout << "年龄:" << this->age << endl;
cout << "学号:" << this->id << endl;
}
struct stu_data
{
string name;
string sex;
int age;
int id;
};
stu_data S_data[5] ={
{"张三" , "男" , 18 , 1},
{"张四" , "男" , 18 , 2},
{"张五" , "男" , 18 , 3},
{"张六" , "男" , 18 , 4},
{"张七" , "男" , 18 , 5},
};
int main() {
Student S_arr[5];
// 写入数据
for (int i = 0; i < 5; ++i) {
S_arr[i].set_data( S_data[i].name , S_data[i].sex \
, S_data[i].age , S_data[i].id);
}
// 打印数据
for (int i = 0; i < 5; ++i) {
S_arr[i].prt_data();
cout << "--------------------" << endl;
}
return 0;
}
3.4.3 重名问题
当成员函数中的形参和成员变量重名时,我们在函数内部调用参数时 如果没有 加this
则代表是对形参进行操作,如下。
void Student::set_data(string name , string sex, int age , int id )
{
// name = name; // 形参 自己赋值 给 自己 都是调用的形参
this->name = name; // 形参 赋值给成员对象
}
3.4.4 空指针访问
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
class test_this{
public:
void func_1()
{
if (this == NULL) {
return;
}
cout << this->id << endl;
return ;
}
void func_2()
{
cout << id << endl;
}
private:
int id;
}
int main()
{
test_this * ptr = NULL;
ptr
}
3.5 封装练习
3.5.1 封装一个 Circle
类
要求如下
******************************
编写一个圆类
成员变量
int r;
int x;
int y;
成员函数
返回周长
返回面积
返回直径
返回圆心
设定半径
设定x
设定y
判定两个圆是否相切
******************************
3.5.2 封装一个 Student
类
要求如下
*******************************
编写一个类 学生类
成员变量
学生姓名
学生成绩
学生学号
成员函数
查看成绩
学生管理类
成员变量
学生类 数组
成员函数
增加学生
删
改
查
*******************************
第四章 面向对象-继承
面向对象的三大特征 封装 集成 多态
其中 ,描述类与类之间的关系 就是我们现在要学习的继承
4.1 继承的概念
基于一个已有的类 去重新定义一个新的类,这种方式我们叫做继承。
意义
可以实现代码复用,减少重复代码的劳动量。
继承是实现多态的必要条件。
继承本身就是为了实现多态的,顺便实现了代码复用。
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码。
关于继承的称呼
一个类B 继承来自 类 A 我们一般称呼。
A类:父类 基类
B类: 子类 派生类
B继承自A A 派生了B
基本语法
class vehicle // 车类
{
}
class car:public vehicle
{
}
4.2 基本语法
class 子类名称:继承方式 父类名称
{
子类新增的成员;
}
class B:public A
{
}
示例程序
#include <iostream>
#include <string>
using namespace std;
// 父类
class Person {
protected:
string name; // 姓名
int age; // 年龄
public:
// 构造函数
Person(string name, int age) : name(name), age(age) {}
// 显示个人信息
void display() const {
cout << "姓名: " << name << ", 年龄: " << age << endl;
}
};
// 子类
class Student : public Person {
private:
int studentID; // 学号
public:
// 构造函数
Student(string name, int age, int studentID) : Person(name, age), studentID(studentID) {}
// 显示学生信息
void show() const {
display(); // 调用父类的 display 函数
cout << "学号: " << studentID << endl;
}
};
int main() {
Student s1("张三", 20, 1001);
s1.show(); // 输出学生的信息,包括姓名、年龄和学号
return 0;
}
4.2.1 基类对象初始化
在子类的构造函数中,通常需要在初始化列表中显式调用基类的构造函数,以完成对基类成员的初始化。例如:
// 在继承时,需要在子类的构造函数的初始化表中 显性的调用父类的构造函数 来完成对父类成员的初始化
格式
构造函数名(int val_1 , int val_2): class_A(val_1 , val_2);
4.2.2 派生类中调用基类
在派生类中可以通过以下方式调用基类的成员:
类外访问:
对象名.基类名::变量;
对象名.基类名::函数名(函数的实参表);
类内访问:
基类名::变量;
基类名::函数名(函数的实参表);
4.3 继承方式
复习访问控制权限
下面是类中的访问控制权限
类内 | 子类 | 类外 | |
---|---|---|---|
public | √ | √ | √ |
protected | √ | √ | × |
private | √ | × | × |
而在继承中 也有三种访问方式:public、protected、private
- 公共继承 public
- 保护继承 protected
- 私有继承 private
他们之间的访问关系 如下图

4.4 继承模型
实际开发中 一般继承都采用 pubilc
继承方式
如果不写继承方式,默认使用的都是 private
方式继承
1、子类中会继承父类中所有成员 ,包括私有成员,只不过在子类中不能直接访问父类私有成员
需要父类提供公有的函数来访问父类的私有成员
2、当父子类中出现了同名的函数时,访问来也不会冲突
即使形参不同,也不构成重载关系 愿意是两个函数不在同一个空间内
如果想访问父类的成员 需要加 类名 :: 来修饰
类外访问:
对象名.基类名::变量;
对象名.基类名::函数名(函数的实参表);
类内访问:
基类名::变量;
对象名.基类名::函数名(函数的实参表);
3、父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
4.5 构造和析构
4.5.1 构造函数
1、父类的构造函数不会被子类继承
2、需要在子类的构造函数的初始列表中,显性的调用父类函数的构造函数
完成对父类中继承过来的成员初始化
3、如果没有在子类的构造函数初始化列表中调用父类中的构造函数 则使用无参构造
如果父类没有无参构造 会报错
4、构造函数的调用顺序是
先调用父类函数的构造函数
再调用子类函数的构造函数
4.5.2 析构函数
1、父类的析构函数不会被子类继承
2、不管是否显性调用父类的析构函数,父类的析构函数都会被调用
完成对父类中继承过来的成员的善后工作
3、子类的析构函数中 无需调用父类的析构函数
4、析构函数的调用顺序是
先调用子类函数的析构函数
再调用父类函数的析构函数
4.5.3 拷贝构造
1、如果子类中没有显性的定义拷贝构造函数,编译器会给子类提供一个默认的拷贝构造函数,而且默认提供的拷贝构造函数,会自动调用父类的拷贝构造函数,完成对从父类中继承过来的成员的初始化。
2、如果子类中显性的定义了拷贝构造函数,需要在子类的拷贝构造函数的初始化表中显性的调用父类的拷贝构造函数。
如果没有显性的调用父类的拷贝构造函数,默认会调用父类的无参构造函数来完成对从父类中继承过来的成员的初始化。
3、如果父类中没有指针成员,使用默认的拷贝构造函数
但是如果父类中有指针, 则需要考虑一下深浅拷贝的问题
4.5.4 调用顺序
继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
4.6 多继承语法
一个子类合一有多个直接父类共同派生,这种派生方式,叫做多重继承
子类会继承每个基类的成员
格式
构造函数的调用顺序 和 声明的顺序有关
class 子类名:继承方式 父类名 , 继承方式 父类名 .....
4.7 菱形继承
第五章 面向对象-多态
5.1 多态的概念
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数==地址早绑定== - 编译阶段确定函数地址 重载
- 动态多态的函数==地址晚绑定== - 运行阶段确定函数地址 重写 : 和虚函数完全一样< 返回值类型 函数名 参数列表>
格式
class Base {
public:
virtual void functionName() { // 虚函数
// 基类实现
}
virtual ~Base() {} // 虚析构函数
};
下面通过案例进行讲解多态
#include <iostream>
#include <string>
using namespace std;
// 基类
class Motor_Ctrl {
public:
// 电机启动
virtual void Moto_start() {
cout << "基类 电机启动" << endl;
}
// 电机停止
virtual void Moto_stop() {
cout << "基类 电机停止" << endl;
}
};
// x 轴电机
class Motor_x : public Motor_Ctrl {
public:
// 重写电机启动
void Moto_start() override {
cout << "x 轴电机启动" << endl;
}
// 重写电机停止
void Moto_stop() override {
cout << "x 轴电机停止" << endl;
}
};
// y 轴电机
class Motor_y : public Motor_Ctrl {
public:
// 重写电机启动
void Moto_start() override {
cout << "y 轴电机启动" << endl;
}
// 重写电机停止
void Moto_stop() override {
cout << "y 轴电机停止" << endl;
}
};
// 函数:电机控制功能
void Motor_func(Motor_Ctrl &motor) {
motor.Moto_start();
motor.Moto_stop();
}
int main() {
Motor_x M_1; // 创建 x 轴电机对象
Motor_y M_2; // 创建 y 轴电机对象
cout << "控制 x 轴电机:" << endl;
Motor_func(M_1); // 控制 x 轴电机
cout << "\n控制 y 轴电机:" << endl;
Motor_func(M_2); // 控制 y 轴电机
return 0;
}
5.2 基本语法
5.2.1 基类语法
基类定义:使用虚函数声明允许子类重写。
class Base {
public:
virtual void show() {
cout << "基类显示" << endl;
}
};
5.2.2 派生类重写
派生类重写:使用 override
关键字(可选)实现方法重写。
当然也可以在重写的函数前面增加virtual
关键词
使用
override
可以增强代码的可读性和安全性,因为它确保你确实是在重写一个虚函数,如果基类中没有对应的虚函数,编译器会报错。
class Derived : public Base {
public:
virtual void show() override {
cout << "派生类显示" << endl;
}
};
5.2.3 多态的使用条件
多态满足条件
- 有继承关系
- 子类重写父类中的虚函数
多态使用条件
- 父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
5.3 纯虚函数
在多态的构建中,父类的虚函数一般是不使用的,也就是说,这个函数是没有意义的,主要是用于子类的重写。
由于这个问题,我们就可以将父类的虚函数改成纯虚函数。
语法
// 纯虚函数语法
virtual 返回值类型 函数名 (参数列表) = 0;
5.4 抽象类
在前面我们知道了纯虚函数,那么现在我们就需要了解 C++ 中 多态的另一个很重要的特征,==抽象类==。
抽象类:当我们的类中,有一个纯虚函数后,那么现在这个类也就被称为了抽象类。
特点:
1、抽象类无法实例化对象
2、抽象类的子类必须重写抽象类的纯虚函数,否则也属于抽象类
3、在函数的形参中 , 还是可以用 抽象类作为 引用参数
如下
// 可以
void func(抽象类 & 类名) //
// 不可以
void func(抽象类 类名) // 实例化对象
#include <iostream>
#include <string>
// 定义一个抽象类
class Shape {
public:
// 纯虚函数,子类必须实现
virtual void draw() = 0;
// 非虚函数,子类可以选择性地重写
void info() {
std::cout << "This is a shape." << std::endl;
}
};
// 继承自 Shape 的子类 Circle
class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};
// 继承自 Shape 的子类 Square
class Square : public Shape {
public:
void draw() override {
std::cout << "Drawing a square." << std::endl;
}
};
int main() {
// 创建 Circle 和 Square 的实例
Shape* circle = new Circle();
Shape* square = new Square();
// 调用 draw 方法
circle->draw(); // 输出: Drawing a circle.
square->draw(); // 输出: Drawing a square.
// 调用 info 方法
circle->info(); // 输出: This is a shape.
square->info(); // 输出: This is a shape.
// 释放内存
delete circle;
delete square;
return 0;
}
5.5 多态模型
在了解这个底层逻辑之前,我们需要先了解两种指针 和 一个概念 ,分别是 vptr
和 vtable
两种指针 以及 动态绑定这个概念。
vfptr vftable
5.5.1 虚函数表vtable
定义:每个包含虚函数的类都有一个虚函数表,它是一个指向虚函数的指针数组。
内容:表中存储了类的所有虚函数的地址。当一个类被继承并重写了某些虚函数时,子类的虚函数表会存储它们的新实现。
5.5.2 虚指针vptr
定义:每个对象在创建时会包含一个虚指针(vptr),它指向该对象所属类的虚函数表。
作用:通过 vptr,程序可以在运行时确定应该调用哪个具体的虚函数。
5.5.3 动态绑定
继承问题:当子类继承了父类(虚函数),也会一并的拷贝父对象的全部内容,包括
vtable
以及vptr
这两个指针,同时vtable
中对函数的指向也会一一拷贝
创建对象:当对象创建的时候,构造函数会初始化这个
vptr
指针,让这个指针指向自己类的vtable
,这个就是我们重写的过程。
调用过程:当我们创建好对象并通过指针的方式传入给父对象时,此时就相当于将我们自身的
vptr
指针做了一个传入,所以我们调用的是子对象的函数。
5.6 虚析构和纯虚析构
C++ 中的虚析构函数和纯虚析构函数用于确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而避免资源泄漏。
作用:可以解决当父类指针指向子类对象时,析构时无法正确调用子类的析构函数的问题。
实现:都需要提供具体的函数实现。
纯虚析构:使类成为抽象类,无法实例化该类的对象。
#include <iostream>
using namespace std;
class Base {
public:
// 虚析构函数
virtual ~Base() {
cout << "Base Destructor Called" << endl;
}
};
class Derived : public Base {
public:
~Derived() override {
cout << "Derived Destructor Called" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 确保调用 Derived 的析构函数
return 0;
}
第六章 面向对象-扩展
6.1 友元
关键词
friend
可以访问朋友类中的私有成员
关于友元的三种实现
* 全局函数做友元
* 类做友元
* 成员函数做友元
6.1.1 友元类
#include <iostream>
#include <string>
/*
* 全局函数做友元
* */
class Date;
using namespace std;
class Time
{
private:
int hour;
int min;
int sec;
public:
// 构造函数 有参构造
Time(int hour , int min , int sec) : hour(hour) , min(min) , sec(sec) {}
// 析构函数
~Time() {}
// 类做友元
friend class Date;
};
class Date
{
private:
int year;
int month;
int day;
public:
// 构造函数
Date(int year , int month , int day) : year(year) , month(month) , day(day) {}
// 析构函数
~Date() {}
// 打印时间和日期
void show_Time_Date(Time & T)
{
// 打印日期
cout << this->year << "/" << this->month << "/" << this->day << endl;
// 打印时间
cout << T.hour << ":" << T.sec << ":" << T.min << endl;
}
};
int main() {
// 时间
Time T1(17 , 14 , 00);
// 日期
Date D1(2025 , 01 , 16);
D1.show_Time_Date(T1);
return 0;
}
6.1.2 友元函数
1、全局函数做友元函数
声明一个函数是类的友元函数,那么这个函数中就可以访问类的私有成员了
#include <iostream>
#include <string>
/*
* 全局函数做友元
* */
using namespace std;
class Time
{
private:
int hour;
int min;
int sec;
public:
// 构造函数 无参构造
Time()
:hour(0) , min(0) , sec(0)
{
}
// 构造函数 有参构造
Time(int hour , int min , int sec) : hour(hour) , min(min) , sec(sec) {}
// 析构函数
~Time() {}
// 打印时间
friend void Show_Time(Time & T);
};
void Show_Time(Time & T)
{
cout << T.hour << ":" << T.min << ":" << T.sec << endl;
}
int main() {
Time T1(10 , 50 , 30);
Show_Time(T1);
return 0;
}
2、成员函数做友元函数
#include <iostream>
#include <string>
/*
* 成员函数做友元
* */
class Date;
using namespace std;
class Time
{
private:
int hour;
int min;
int sec;
public:
// 构造函数 有参构造
Time(int hour , int min , int sec) : hour(hour) , min(min) , sec(sec) {}
// 析构函数
~Time() {}
//
void show_Date_Time(Date & D);
};
class Date
{
private:
int year;
int month;
int day;
public:
// 构造函数
Date(int year , int month , int day) : year(year) , month(month) , day(day) {}
// 析构函数
~Date() {}
friend void Time::show_Date_Time(Date & D);
};
void Time::show_Date_Time(Date & D)
{
// 打印日期
cout << this->hour << ":" << this->min << ":" << this->sec << endl;
// 打印时间
cout << D.year << "/" << D.month << "/" << D.day << endl;
}
int main() {
Time T1(10 , 50 , 30);
Date D1(2025 , 1 , 16);
T1.show_Date_Time(D1);
return 0;
}
6.1.2 友元的注意事项
- 友元关系不具有交换性:A是B的朋友,B不一定是A的朋友。
- 友元关系不具有传递性:A是B的朋友,B是C的朋友,A不一定是C的朋友。
- 友元关系不能被继承:父类的朋友不一定是子类的朋友。
- 友元关系破坏了类的封装性,让访问控制权限失去意义了。
在实际开发中,不要过度依赖友元。
6.2 常对象
6.2.1 常成员函数
常成员函数中不允许修改成员变量的值
在常成员函数中,不能修改类的成员变量,除非该成员变量被声明为 mutable
。
常成员函数的格式
常成员函数的格式为:
返回值类型 成员函数名(函数的参数列表) const
{
函数体;
}
例如:
void show() const { ... }
=
常成员函数支持重载
常成员函数可以与非常成员函数一起构成重载关系。例如,同名函数,如果一个是常成员函数,另一个是非常成员函数,它们可以共存,并依据调用的对象是否为 const
类型来选择调用哪个版本。
常成员函数中的 this
指针被 const
修饰
在常成员函数中,this
指针的类型是 const 类名* const
,即指针和指向的对象都是常量。而在非常成员函数中,this
的类型是 类名* const
,指针指向的对象可以被修改。
非常成员函数的 this
指针类型:
类名 * const this;
常成员函数中的 this
指针类型:
const 类名 * const this;
例子:
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
string name;
string sex;
int id;
public:
// 构造函数
Student() : name("无名"), sex("男"), id(-1) {}
Student(string name, string sex, int id) : name(name), sex(sex), id(id) {}
// 常成员函数
void show() const {
// 此处无法修改成员变量值,因函数被 const 修饰
cout << "姓名: " << name << " 性别: " << sex << " 学号: " << id << endl;
}
};
int main() {
Student S1("张三", "男", 1);
S1.show(); // 调用常成员函数
return 0;
}
6.2.2 常对象
常对象的定义
在实例化类对象时,使用 const
修饰该对象,则该对象为常对象。
常对象格式:
const 类名 对象名(构造函数参数表);
类名 const 对象名(构造函数参数表);
常对象只能调用常成员函数
常对象只能调用类中的 ==常成员函数==,因为常成员函数保证不会修改对象的成员变量。而非常对象可以调用非常成员函数和常成员函数。
常成员函数与非常成员函数的重载:
class Student {
public:
// 常成员函数
void show() const {
cout << "常成员函数" << endl;
}
// 非常成员函数
void show() {
cout << "非常成员函数" << endl;
}
};
常对象 只能调用常成员函数:
const Student S2;
S2.show(); // 调用常成员函数
非常对象既可以调用常成员函数,也可以调用非常成员函数:
Student S1;
S1.show(); // 调用非常成员函数
引用与常对象的访问规则:
如果一个非常对象被常引用引用,则通过引用访问时,按常对象的方式进行访问;通过对象本身访问时,按非常对象的方式访问。
Student S1; // 非常对象
const Student &S2 = S1; // 常引用指向非常对象
S1.show(); // 调用非常成员函数
S2.show(); // 调用常成员函数
通过对象 S1
是非常对象的方式访问,而通过引用 S2
是常对象的方式访问,因此调用了不同版本的 show
函数。
6.3 匿名对象
在C++中,匿名对象是指那些没有显式命名的对象,通常用于临时性操作或在函数调用时不需要保留的对象。它们在程序运行过程中短暂地存在,用于表达式的计算或者函数的参数传递。匿名对象的典型特征是生命周期极短,通常在它们的作用范围结束时会被销毁,不需要程序员显式地管理它们的内存。
匿名对象的特点
-
短生命周期
-
简化代码
-
自动销毁
示例代码
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
int age;
Student(string n, int a) : name(n), age(a) {}
void show() {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
void func(Student s) {
s.show();
}
int main() {
// 匿名对象可能的使用场景:
// 1. 初始化类数组的时候
Student hqyj[3] = {Student("小明", 18), Student("小红", 15), Student("小李", 25)};
hqyj[0].show();
hqyj[1].show();
hqyj[2].show();
// 2. 给函数传参时
func(Student("张飞", 40));
return 0;
}
6.4 静态成员
静态成员可以分为静态成员变量和静态成员函数,具体如下:
静态成员变量
- 共享性:所有对象共享同一份数据。
- 内存分配:在编译阶段分配内存,而不是在对象创建时。
- 初始化方式:在类内声明,类外进行初始化。
静态成员函数
- 共享性:所有对象共享同一个函数。
- 访问限制:静态成员函数只能访问静态成员变量,不能访问非静态成员变量和非静态成员函数。
- 此外 由于 静态成员函数只能访问静态成员变量 所以不能使用
this
指针
6.4.1 静态成员变量
#include <iostream>
#include <string>
using namespace std;
// 声明类
class Student
{
public:
static int val_1;
private:
static int val_2;
};
// 全局声明
int Student::val_1 = 100;
int Student::val_2 = 200;
int main()
{
// 静态成员的访问方式
// 1、通过对象访问
Student S_1;
Student S_2;
S_1.val_1 = 110;
cout << "S_1 val_1 = " << S_1.val_1 << endl;
S_2.val_1 = 120;
cout << "S_1 val_1 = " << S_1.val_1 << endl;
cout << "S_2 val_1 = " << S_2.val_1 << endl;
// 2、通过类名访问
cout << "val_1 = " << Student::val_1 << endl;
// cout << "val_2 = " << Student::val_2 << endl;
return 0;
}
6.4.2 静态成员函数
#include <iostream>
#include <string>
using namespace std;
// 声明类
class Student
{
public:
static int val_1;
private:
static int val_2;
public:
int val_3;
static void func(int val )
{
val_1 = val; // 可以使用的
// val_3 = val; // 不可以使用
cout << "调用 func " << " val_1 = " << val_1 << endl;
}
};
// 全局声明
int Student::val_1 = 100;
int Student::val_2 = 200;
int main()
{
// 静态成员的访问方式
// 1、通过对象访问
Student S_1;
S_1.func(80);
// 2、通过类名访问
Student::func(90);
return 0;
}
6.5 运算符重载
6.5.1 概述
6.5.2 算数运算符重载
1、格式
表达式 L # R (L 左操作数 # 运算符 R 右操作数)
左操作数:既可以是左值 也可以是右值
右操作数:既可以是左值 也可以是右值
返回值的结果:只能是右值
成员函数版本
从编译器角度: L.operator#( R )
const 类名 operator#(cosnt 类名 &R)const;
const 返回值 是一个 右值 不能被修改
const 常函数 用于保护 左操作数
全局函数版本
从编译器的角度: operator#(L , R);
friend const 类名 operator#(const 类名 &L , const 类名 &R);
friend 为了方便访问私有成员
const 返回值 是一个 右值 不能被修改
2、示例
#include <iostream>
#include <string>
using namespace std;
// 复数类
class Complex
{
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex() {}
Complex(double real , double imag) : real(real) , imag(imag) {}
// 析构函数
~Complex() {}
// 打印函数
void show() const
{
cout << "real = " << this->real << endl;
cout << "imag = " << this->imag << endl;
}
void show(string str) const
{
cout << str << ".real = " << this->real << endl;
cout << str << ".imag = " << this->imag << endl;
}
// 重载 + 加法运算符 成员函数版本
const Complex operator + (const Complex & other)
{
Complex temp;
temp.real = this->real + other.real;
temp.imag = this->imag + other.real;
return temp;
}
// 重载 - 减法运算符 全局函数版本
friend const Complex operator - (const Complex & L , const Complex & R);
};
const Complex operator - (const Complex & L , const Complex & R)
{
Complex temp;
temp.real = L.real - R.real;
temp.imag = L.imag - R.imag;
return temp;
}
int main()
{
Complex C1(1 , 2);
Complex C2(3 , 4);
C1.show(string("C1"));
C2.show(string("C2"));
Complex C3 = C1 + C2;
C3.show(string("C3"));
return 0;
}
6.5.3 自增自减运算符重载
1、格式
左操作数
右操作数
操作数 左值
结果 左值 右值
前缀
++a --a
表达式 #a (a 操作数 # 运算符)
操作数 只能是左值
表达式的结果 只能是左值 (因为 C++ 中允许 ++++a 允许前缀级联自增)
格式
成员函数
从编译器的角度: operator#(void)
类名 & operator#(void)
全局函数
从编译器的角度: operator#(类名 &)
friend 类名 & operator++(void);
后缀
a++ a--
表达式 a# (a 操作数 # 运算符)
操作数 只能是左值
表达式的结果 只能是右值 (因为 C++ 中不允许 a++++ 不允许前缀级联自增)
格式
成员函数版本
从编译器的角度 L.operator#(int)
const 类名 operator#(int); int 只作为哑元使用 起到占位的作用 用来区分前缀和后缀
全局函数版本
从编译器的角度 operator#(O)
friend const 类名 operator++(类名 &O , int);
2、示例
#include <iostream>
#include <string>
using namespace std;
// 复数类
class Complex
{
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex() {}
Complex(double real , double imag) : real(real) , imag(imag) {}
// 析构函数
~Complex() {}
// 打印函数
void show() const
{
cout << "real = " << this->real << endl;
cout << "imag = " << this->imag << endl;
}
void show(string str) const
{
cout << str << ".real = " << this->real << endl;
cout << str << ".imag = " << this->imag << endl;
}
// 重载 ++ 前缀自增 返回的是引用 是为了返回自身
Complex & operator++(void)
{
++this->real;
++this->imag;
return *this;
}
// 重载 -- 后缀自减 这里不能返回引用,应为返回的是值
Complex operator++(int)
{
// 先记录
Complex temp = *this;
// 在进行递增
++this->real;
++this->imag;
// 最后返回
return temp;
}
};
int main()
{
Complex C1(1 , 2);
C1.show(string("C1"));
return 0;
}
6.5.4 位操作运算符重载
对于位操作运算符 我们主要研究他 的 ==单目运算符== 和 ==左移运算符==
1、格式
表达式
表达式 #a (a 操作数 # 运算符)
操作数 只能是左值
表达式的结果 只能是右值
成员函数版本
从编译器的角度 L.operator#(R)
const 类名 operator#() const;
全局函数版本
从编译器的角度 operator#(O)
friend const 类名 operator#(类名 & O);
2、示例 单目运算符
#include <iostream>
using namespace std;
class Complex
{
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex(double real = 0, double imag = 0) : real(real), imag(imag) {}
// 重载取反运算符
bool operator!() const
{
return (real == 0 && imag == 0);
}
// 显示复数
void show() const
{
cout << "real = " << real << ", imag = " << imag << endl;
}
};
int main()
{
Complex c1(0, 0);
Complex c2(1, 2);
if (!c1) {
cout << "c1 is zero." << endl;
} else {
cout << "c1 is non-zero." << endl;
}
if (!c2) {
cout << "c2 is zero." << endl;
} else {
cout << "c2 is non-zero." << endl;
}
return 0;
}
3、示例 左移运算符
#include <iostream>
using namespace std;
class Complex {
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex(double real = 0, double imag = 0) : real(real), imag(imag) {}
// 友元全局函数重载 << 运算符
friend ostream& operator<<(ostream& os, const Complex& c);
// 可用于其他操作的成员函数
double getReal() const { return real; }
double getImag() const { return imag; }
};
// 全局函数重载 << 运算符
ostream& operator<<(ostream& os, const Complex& c) {
os << "(" << c.real << " + " << c.imag << "i)";
return os; // 返回流对象以支持链式操作
}
int main() {
Complex c1(3, 4);
Complex c2(1, -2);
cout << "c1 = " << c1 << endl; // 使用重载的 << 输出 c1
cout << "c2 = " << c2 << endl; // 使用重载的 << 输出 c2
return 0;
}
6.5.5 关系运算符重载
1、格式
表达式 L # R (L 左操作数 # 运算符 R 右操作数)
左操作数: 既可以是左值 也可以是右值
右操作数: 既可以是左值 也可以是右值
返回值的结果: 只能是右值 bool 类型
成员函数
从编译器角度: L.operator#( R )
const bool operator#(cosnt 类名 &R)const;
const 返回值 是一个 右值 不能被修改
const 常函数 用于保护 左操作数
全局函数版本
从编译器的角度: operator#(L , R);
friend const bool operator#(const 类名 &L , const 类名 &R);
friend 为了方便访问私有成员
const 返回值 是一个 右值 不能被修改
2、示例
#include <iostream>
#include <cmath>
using namespace std;
class Complex {
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex(double real = 0, double imag = 0) : real(real), imag(imag) {}
// 成员函数重载 ==
bool operator==(const Complex &other) const {
return (real == other.real && imag == other.imag);
}
// 成员函数重载 <
bool operator<(const Complex &other) const {
return (std::sqrt(real * real + imag * imag) < std::sqrt(other.real * other.real + other.imag * other.imag));
}
// 友元全局函数重载 !=
friend bool operator!=(const Complex &c1, const Complex &c2) {
return !(c1 == c2); // 利用已定义的 == 运算符
}
// 友元全局函数重载 >
friend bool operator>(const Complex &c1, const Complex &c2) {
return (c2 < c1); // 利用已定义的 < 运算符
}
void show() const {
cout << "(" << real << " + " << imag << "i)" << endl;
}
};
int main() {
Complex c1(3, 4);
Complex c2(3, 4);
Complex c3(1, 2);
cout << "c1 == c2: " << (c1 == c2) << endl; // 输出 1 表示 true
cout << "c1 != c3: " << (c1 != c3) << endl; // 输出 1 表示 true
cout << "c1 < c3: " << (c1 < c3) << endl; // 输出 0 表示 false
cout << "c1 > c3: " << (c1 > c3) << endl; // 输出 1 表示 true
return 0;
}
6.5.6 赋值运算符重载
关于拷贝赋值函数:
拷贝赋值函数 和构造析构函数一样, 都是属于编译器会默认提供一个成员函数,所以编译器提供了成员函数版本的拷贝赋值函数,我们在编写的时候,就不能写全局函数版本的拷贝赋值函数,只能存一。
而拷贝赋值函数 就是我们现在要写 等号 运算符重载
1、格式
表达式 L # R (L 左操作数 # 运算符 R 右操作数)
左操作数: 既可以是左值
右操作数: 既可以是左值 也可以是右值
返回值的结果: 只能是左值自身
成员函数版本
从编译器角度: L.operator#( R )
类名 & operator#(cosnt 类名 &R);
全局函数版本
从编译器的角度: operator#(L , R);
friend 类名 operator#(const 类名 &L , const 类名 &R);
2、示例
#include <iostream>
#include <string>
using namespace std;
// 复数类
class Complex
{
private:
double real; // 实部
double imag; // 虚部
public:
// 构造函数
Complex() {}
Complex(double real , double imag) : real(real) , imag(imag) {}
// 析构函数
~Complex() {}
// 打印函数
void show() const
{
cout << "real = " << this->real << endl;
cout << "imag = " << this->imag << endl;
}
void show(string str) const
{
cout << str << ".real = " << this->real << endl;
cout << str << ".imag = " << this->imag << endl;
}
// 赋值拷贝运算符重载
Complex & operator=(const Complex R)
{
this->imag = R.imag;
this->real = R.real;
return *this;
}
};
int main()
{
Complex C1(1 , 2);
Complex C2;
C2 = C1;
C1.show("C1");
C2.show("C2");
return 0;
}
6.5.7 函数调用运算符重载
- 函数调用运算符 () 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
1、格式
2、示例
class MyPrint
{
public:
void operator()(string text)
{
cout << text << endl;
}
};
void test01()
{
//重载的()操作符 也称为仿函数
MyPrint myFunc;
myFunc("hello world");
}
class MyAdd
{
public:
int operator()(int v1, int v2)
{
return v1 + v2;
}
};
void test02()
{
MyAdd add;
int ret = add(10, 10);
cout << "ret = " << ret << endl;
//匿名对象调用
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}
int main() {
test01();
test02();
system("pause");
return 0;
}
第七章 系统编程
7.1 文件编程
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件 ==< fstream >==
文件类型分为两种:
- 文本文件 - 文件以文本的ASCII码形式存储在计算机中
- 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream:写操作
- ifstream: 读操作
- fstream : 读写操作
7.1.2 文本文件
1、 写文件
写文件步骤如下:
-
包含头文件
#include <fstream>
-
创建流对象
ofstream ofs;
-
打开文件
ofs.open("文件路径",打开方式);
-
写数据
ofs << "写入的数据";
-
关闭文件
ofs.close();
文件打开方式:
打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意: 文件打开方式可以配合使用,利用|操作符
**例如:**用二进制方式写文件 ios::binary | ios:: out
示例:
#include <fstream>
void test01()
{
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "姓名:张三" << endl;
ofs << "性别:男" << endl;
ofs << "年龄:18" << endl;
ofs.close();
}
int main() {
test01();
system("pause");
return 0;
}
总结:
- 文件操作必须包含头文件 fstream
- 读文件可以利用 ofstream ,或者fstream类
- 打开文件时候需要指定操作文件的路径,以及打开方式
- 利用<<可以向文件中写数据
- 操作完毕,要关闭文件
2、 读文件
读文件与写文件步骤相似,但是读取方式相对于比较多
读文件步骤如下:
-
包含头文件
#include <fstream>
-
创建流对象
ifstream ifs;
-
打开文件并判断文件是否打开成功
ifs.open("文件路径",打开方式);
-
读数据
四种方式读取
-
关闭文件
ifs.close();
示例:
#include <fstream>
#include <string>
void test01()
{
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}
//第一种方式
//char buf[1024] = { 0 };
//while (ifs >> buf)
//{
// cout << buf << endl;
//}
//第二种
//char buf[1024] = { 0 };
//while (ifs.getline(buf,sizeof(buf)))
//{
// cout << buf << endl;
//}
//第三种
//string buf;
//while (getline(ifs, buf))
//{
// cout << buf << endl;
//}
char c;
while ((c = ifs.get()) != EOF)
{
cout << c;
}
ifs.close();
}
int main() {
test01();
system("pause");
return 0;
}
总结:
- 读文件可以利用 ifstream ,或者fstream类
- 利用is_open函数可以判断文件是否打开成功
- close 关闭文件
7.1.3 二进制文件
以二进制的方式对文件进行读写操作
打开方式要指定为 ==ios::binary==
1、 写文件
二进制方式写文件主要利用流对象调用成员函数write
函数原型 :ostream& write(const char * buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
示例:
#include <fstream>
#include <string>
class Person
{
public:
char m_Name[64];
int m_Age;
};
//二进制文件 写文件
void test01()
{
//1、包含头文件
//2、创建输出流对象
ofstream ofs("person.txt", ios::out | ios::binary);
//3、打开文件
//ofs.open("person.txt", ios::out | ios::binary);
Person p = {"张三" , 18};
//4、写文件
ofs.write((const char *)&p, sizeof(p));
//5、关闭文件
ofs.close();
}
int main() {
test01();
system("pause");
return 0;
}
总结:
- 文件输出流对象 可以通过write函数,以二进制方式写数据
2、 读文件
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
示例:
#include <fstream>
#include <string>
class Person
{
public:
char m_Name[64];
int m_Age;
};
void test01()
{
ifstream ifs("person.txt", ios::in | ios::binary);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
}
Person p;
ifs.read((char *)&p, sizeof(p));
cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}
int main() {
test01();
system("pause");
return 0;
}
- 文件输入流对象 可以通过read函数,以二进制方式读数据
7.2 网络编程
要求
自己编写一个 TCP 客户端的类 以及 服务器的类
服务器的类要求
1、构造与析构 构造函数设置服务器的IP、端口等参数;析构函数关闭连接。
2、启动与关闭 启动方法负责创建、绑定、监听服务器套接字;关闭方法释放资源。
3、客户端连接 支持接受客户端连接,管理多个客户端的连接。
4、数据收发 重载 << 和 >> 操作符,实现向客户端发送和接收数据。
客户端类编写要求
1、构造与析构 构造函数设置服务器IP和端口;析构函数关闭连接。
2、连接与断开 连接方法用于与服务器建立连接;断开方法关闭连接。
3、数据收发 重载 << 和 >> 操作符,实现与服务器的数据通信。
参考
class TCPServer {
private:
int port; // 服务器监听的端口号
int server_fd; // 服务器套接字文件描述符
int client_fd; // 客户端套接字文件描述符
public:
// 构造函数,初始化端口号和套接字描述符
TCPServer(int port);
// 析构函数,关闭服务器以释放资源
~TCPServer();
// 启动服务器,创建套接字并绑定和监听
bool start();
// 接收客户端连接
bool acceptClient();
// 接收客户端发送的数据
bool receiveData(std::string &data);
// 向客户端发送数据
bool sendData(const std::string &data);
// 重载 << 操作符,向客户端发送数据
TCPServer& operator<<(const std::string &data);
// 重载 >> 操作符,从客户端接收数据
TCPServer& operator>>(std::string &data);
};
class TCPClient {
private:
std::string server_ip; // 服务器 IP 地址
int port; // 服务器端口号
int client_fd; // 客户端套接字文件描述符
public:
// 构造函数,初始化服务器 IP 和端口
TCPClient(const std::string &ip, int port);
// 析构函数,关闭客户端连接以释放资源
~TCPClient();
// 连接服务器
bool connectToServer();
// 断开连接
void disconnect();
// 接收服务器发送的数据
bool receiveData(std::string &data);
// 向服务器发送数据
bool sendData(const std::string &data);
// 重载 << 操作符,向服务器发送数据
TCPClient& operator<<(const std::string &data);
// 重载 >> 操作符,从服务器接收数据
TCPClient& operator>>(std::string &data);
};
7.2.1 服务器
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
// TCP 服务器类封装
class TCPServer {
public:
// 构造函数,初始化端口号和套接字描述符
TCPServer(int port) : port(port), server_fd(-1), client_fd(-1) {}
// 析构函数,关闭服务器以释放资源
~TCPServer() {
closeServer();
}
// 启动服务器,创建套接字并绑定和监听
bool start() {
// 创建套接字,AF_INET 表示使用 IPv4 协议,SOCK_STREAM 表示使用 TCP
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return false;
}
// 配置服务器地址结构
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET; // 使用 IPv4
server_addr.sin_addr.s_addr = INADDR_ANY; // 允许任何地址连接
server_addr.sin_port = htons(port); // 端口号,使用网络字节序
// 绑定套接字到指定端口
if (bind(server_fd, (sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "Bind failed" << std::endl;
close(server_fd);
return false;
}
// 启动监听,最多允许 3 个连接请求等待队列
if (listen(server_fd, 3) < 0) {
std::cerr << "Listen failed" << std::endl;
close(server_fd);
return false;
}
std::cout << "Server listening on port " << port << std::endl;
return true;
}
// 接收客户端连接
bool acceptClient() {
sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
// 接收客户端连接请求,阻塞等待直到有连接到达
client_fd = accept(server_fd, (sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
std::cerr << "Accept failed" << std::endl;
return false;
}
std::cout << "Client connected" << std::endl;
return true;
}
// 接收客户端发送的数据
std::string receiveData() {
// 检查是否有客户端连接
if (client_fd < 0) {
std::cerr << "No client connected" << std::endl;
return "";
}
char buffer[1024] = {0}; // 接收缓冲区
ssize_t bytesReceived = recv(client_fd, buffer, sizeof(buffer), 0); // 接收数据
if (bytesReceived < 0) {
std::cerr << "Receive failed" << std::endl;
return "";
}
// 将接收的数据转换为字符串并返回
return std::string(buffer, bytesReceived);
}
// 向客户端发送数据
void sendData(const std::string &data) {
// 检查是否有客户端连接
if (client_fd < 0) {
std::cerr << "No client connected" << std::endl;
return;
}
// 发送数据给客户端
send(client_fd, data.c_str(), data.size(), 0);
}
// 关闭服务器和客户端连接
void closeServer() {
if (client_fd >= 0) {
close(client_fd); // 关闭客户端连接
client_fd = -1;
}
if (server_fd >= 0) {
close(server_fd); // 关闭服务器套接字
server_fd = -1;
}
}
private:
int port; // 服务器监听的端口号
int server_fd; // 服务器套接字文件描述符
int client_fd; // 客户端套接字文件描述符
};
// 主函数,测试 TCPServer 类的功能
int main() {
TCPServer server(8080); // 创建一个监听 8080 端口的服务器
// 启动服务器
if (!server.start()) {
return -1;
}
// 等待客户端连接
if (!server.acceptClient()) {
return -1;
}
// 接收客户端发送的信息
std::string message = server.receiveData();
std::cout << "Received: " << message << std::endl;
// 向客户端发送一条消息
server.sendData("Hello from server");
// 关闭服务器
server.closeServer();
return 0;
}
7.2.2 客户端
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
// TCP 客户端类封装
class TCPClient {
public:
// 构造函数,初始化服务器的 IP 地址和端口号
TCPClient(const std::string &server_ip, int port) : server_ip(server_ip), port(port), sock_fd(-1) {}
// 析构函数,关闭客户端连接以释放资源
~TCPClient() {
closeConnection();
}
// 连接到服务器
bool connectToServer() {
// 创建套接字,AF_INET 表示使用 IPv4 协议,SOCK_STREAM 表示使用 TCP
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return false;
}
// 配置服务器地址结构
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET; // 使用 IPv4
server_addr.sin_port = htons(port); // 端口号,使用网络字节序
// 将 IP 地址转换为网络字节序并存储
if (inet_pton(AF_INET, server_ip.c_str(), &server_addr.sin_addr) <= 0) {
std::cerr << "Invalid address or Address not supported" << std::endl;
close(sock_fd);
return false;
}
// 连接服务器
if (connect(sock_fd, (sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "Connection failed" << std::endl;
close(sock_fd);
return false;
}
std::cout << "Connected to server" << std::endl;
return true;
}
// 发送数据到服务器
void sendData(const std::string &data) {
if (sock_fd < 0) {
std::cerr << "No connection established" << std::endl;
return;
}
// 发送数据到服务器
send(sock_fd, data.c_str(), data.size(), 0);
}
// 从服务器接收数据
std::string receiveData() {
if (sock_fd < 0) {
std::cerr << "No connection established" << std::endl;
return "";
}
char buffer[1024] = {0}; // 接收缓冲区
ssize_t bytesReceived = recv(sock_fd, buffer, sizeof(buffer), 0); // 接收数据
if (bytesReceived < 0) {
std::cerr << "Receive failed" << std::endl;
return "";
}
return std::string(buffer, bytesReceived);
}
// 关闭客户端连接
void closeConnection() {
if (sock_fd >= 0) {
close(sock_fd); // 关闭客户端连接
sock_fd = -1;
}
}
private:
std::string server_ip; // 服务器的 IP 地址
int port; // 服务器的端口号
int sock_fd; // 客户端套接字文件描述符
};
// 主函数,测试 TCPClient 类的功能
int main() {
TCPClient client("127.0.0.1", 8080); // 创建一个连接到 127.0.0.1:8080 的客户端
// 连接到服务器
if (!client.connectToServer()) {
return -1;
}
// 向服务器发送一条消息
client.sendData("Hello from client");
// 接收服务器的回应
std::string message = client.receiveData();
std::cout << "Received: " << message << std::endl;
// 关闭客户端连接
client.closeConnection();
return 0;
}
- 感谢你赐予我前进的力量