c++学习
README
项目开源来自gitee.com/ASUS_HACKED
有问题请询问科协-网络安全部ss0t行动小组 HACKED(ss0t_hacked@qq.com)
想写这个教程其实从2025年就开始了,当时没什么动力去写,而且对程序设计并不是有一个系统性的学习。后面才发现程序设计越来越重要,学校的衔接的知识点太过于简单和跳跃性(跳了很多计的基础大,导致一时看不懂c++的代码)
不过现在是复习,同时也是我对C++的基础知识的理解,如果你遇到问题的话也可以来找我,或者出现问题需要反馈也可以联系我。希望你能对程序设计有一个全新的看法和感兴趣。
如果喜欢程序设计的话也可以加入科协的算法部门和项目部门,感兴趣的也可以咨询我
本教程的书写环境是VS2022,里面的代码文件均为.cpp格式,支持的编译器有dev-c++和code:block(太老太难用了)
首先我先默认你已经初步的学习了c的相关知识,并且学到了运算符一类的东西,如果没有的话请你先学习C语言的内容再回来看这部分的复习
推荐学习网站是:
https://www.runoob.com/cplusplus/cpp-tutorial.html
https://c.biancheng.net/view/2188.html
部分涉及到的知识点都会从上面出来,例子我会进行举例子
C++ 读作“C加加”,是“C Plus Plus”的简称。顾名思义,C++ 是在C语言的基础上增加新特性,玩出了新花样,所以叫“C Plus Plus”,就像 iPhone 7S 和 iPhone 7、Win10 和 Win7 的关系。
从语法上看,C语言是 C++ 的一部分,C语言代码几乎不用修改就能够以 C++ 的方式编译,这给很多初学者带来了不小的困惑,学习 C++ 之前到底要不要先学习C语言呢?
我对这个问题保持中立,但是初学者直接学习 C++ 会非常吃力,Hold 不住,尤其是对计算机内存不太理解的情况下,C++ 是学不懂的。C++ 是一门灵活多变、特性丰富的语言,同时也意味着比较复杂,不易掌握。
不过可以明确地说:学了C语言就相当于学了 C++ 的一半,从C语言转向 C++ 时,不需要再从头开始,接着C语言往下学就可以,所以我强烈建议先学C语言再学 C++。
C++和C语言的血缘关系
现在看来,C++ 和C语言虽然是两门独立的语言,但是它们却有着扯也扯不清的关系。
早期并没有“C++”这个名字,而是叫做“带类的C”。“带类的C”是作为C语言的一个扩展和补充出现的,它增加了很多新的语法,目的是提高开发效率,如果你有 Java Web 开发经验,那么你可以将它们的关系与 Servlet 和 JSP 的关系类比。
这个时期的 C++ 非常粗糙,仅支持简单的面向对象编程,也没有自己的编译器,而是通过一个预处理程序(名字叫 cfront),先将 C++ 代码”翻译“为C语言代码,再通过C语言编译器合成最终的程序。
随着 C++ 的流行,它的语法也越来越强大,已经能够很完善的支持面向过程编程、面向对象编程(OOP)和泛型编程,几乎成了一门独立的语言,拥有了自己的编译方式。
我们很难说 C++ 拥有独立的编译器,例如 Windows 下的微软编译器(cl.exe)、Linux 下的 GCC 编译器、Mac 下的 Clang 编译器(已经是 Xcode 默认编译器,雄心勃勃,立志超越 GCC),它们都同时支持C语言和 C++,统称为 C/C++ 编译器。对于C语言代码,它们按照C语言的方式来编译;对于 C++ 代码,就按照 C++ 的方式编译。
从表面上看,C、C++ 代码使用同一个编译器来编译,所以上面我们说“后期的 C++ 拥有了自己的编译方式”,而没有说“C++ 拥有了独立的编译器”。
再说C++教程
如果针对没有任何编程经验的读者写一本 C++ 的书,那将是一项不小的任务,写出来的书也会非常厚。即使这样,也仅仅是在讲语法。
更重要的是,这些知识你很难全部吸收,会严重打击你的信心,失去学习的兴趣。
特别鸣谢
上古法羊,Limhope以及各位小伙伴建议和支持。
Given enough eyeballs, all bugs are shallow ---开源精神
每个提供建议的人都值得被铭记
每日推荐(doge)

更新日志
2026.03.20
重大更新和测试
一、先行测试代码动画讲解,底层逻辑动画演示
二、更新第2,3,4章动画演示
2026.03.14
一、更新17.结构体const的使用场景
二、更新18.结构体综合练习
2026.03.03
一、更新结构体嵌套结构体
二、更新结构体做函数参数
2026.03.02
一、更新结构体指针
2026.03.01
一、更新结构体数组
2026.02.21
一、修改排版,序号
二、更新12.结构体
2026.02.19
一、更新函数-函数分文件编写
二、11.指针和引用
2026.2.11
一、更新1.11 指针函数和函数指针
二、快速了解c++的不同
三、指向指针的指针
四、指针和常量
五、函数
六、函数指针和指针函数
2026.2.3
一、1.3 定义和声明 添加默认形参
二、增加第二章-快速了解c++
三、调整命名空间顺序,添加函数重载内容
1.快速了解c和c++的不同
1.1 头文件区别
还记得我们学习c语言写的第一段代码吗?
那就是hello,world
下面展示两段代码
#include<stdio.h>
int main()
{
printf("hello,world\n");
return 0;
}
#include<iostream>
int main()
{
std::cout<<"hello,world"<<std::endl;
return 0;
}
再一个手动输入打印字符的代码
include<stdio.h>
int main()
{
char a[100];
scanf("%s",a);//这里没写错哈,因为在c/c++语言中数组名就是数组首元素的地址
printf("%s\n",a);
return 0;
}
#include<iostream>
int main()
{
char a[100];
std::cin>>a;
std::cout<<a<<std::endl;
}
细心的你发现了,c和c++都有共同的特征,那就是分号!(这好像是一个废话了)
c的头文件是stdio.h,c++的头文件是iostream。学长你是不是写错了,怎么iostream不用写.h。(´⊙ω⊙`)
这也是c和c++的区别之一。
c语言中不管是什么类型的头文件(包括自己定义的头文件,都需要写.h)
但是c++中不同的是,只有标准库中没有的东西才需要写,也就是自己定义的头文件,类似于iostream这个头文件是本身这个编译器就有的(但凡是一个c++的编译器,他都会和c语言一样带了一个内置的头文件库)而在C++中,标准库的头文件通常不使用.h扩展名,而是采用了新的命名方式,去掉了扩展名前的stdio等前缀,并直接使用
1.2 变量和常量
变量和常量还记得是什么吧,这些是c的内容,c++完全一样的
| 对比项 | 变量(Variable) | 常量(Constant) |
|---|---|---|
| 定义 | 用来存储可改变的数据 | 用来存储不可改变的数据 |
| 值是否可变 | ✅ 可以在程序运行过程中修改 | ❌ 一旦定义后不能修改 |
| 使用目的 | 表示会变化的数据(计数、输入、状态等) | 表示固定不变的数据(π、配置值等) |
| 安全性 | 较低,可能被误修改 | 较高,防止被意外修改 |
| 内存占用 | 需要内存空间 | 需要内存空间 |
| 编程习惯 | 普通数据使用 | 能不变就尽量用常量 |
| 示例(C) | int a = 10; |
const int a = 10; |
1.3 定义和声明
相信很多人对这个都有疑问吧,这个算是扩展性内容,学校都不会讲de。
声明(Declaration):
👉 告诉编译器“有这么个东西”。
定义(Definition):
👉 告诉编译器“这个东西在这里,并且怎么实现”。
从“编译器视角”看区别(非常关键)
1️⃣ 编译器看到「声明」时
编译器只做三件事:
- 记录名字
- 记录类型/签名
- 检查用法是否合法
❗但是:
- 不分配内存
- 不生成实现代码
就像你跟编译器说:
“别急,这个变量/函数以后会有的。
2️⃣ 编译器看到「定义」时
编译器会:
- 分配内存空间(变量)
- 生成函数的机器码(函数)
- 建立符号和实体的一一对应关系
等于你对编译器说:
“东西就在这,用这个!“
1.3.1 变量:声明 vs 定义(重点)
1️⃣ 变量定义(Definition)
int a = 10;
这是 定义,因为:
- 指定了类型:
int - 指定了名字:
a - 分配了内存
- 还给了初始值
📌 即使没有初始值,也是定义:
int b;
2️⃣ 变量声明(Declaration)
extern int a;
这是 声明,因为:
- 只告诉编译器
a是一个int - 不分配内存
- 实际定义在别的文件中
📌 常见场景:多文件编程
// a.c
int a = 10;
// b.c
extern int a;
1.3.2 函数:声明 vs 定义
1️⃣ 函数声明(函数原型)
int add(int x, int y);
特点:
- 有返回值类型
- 有函数名
- 有参数列表
- 没有函数体
📌 这是声明,不是定义。
2️⃣ 函数定义
int add(int x, int y) {
return x + y;
}
特点:
- 包含函数体
{ } - 编译器会生成可执行代码
📌 函数定义本身也是一次声明
| 项目 | 声明(Declaration) | 定义(Definition) |
|---|---|---|
| 是否分配内存 | 否 | 是 |
| 是否生成代码 | 否 | 是 |
| 是否必须唯一 | 否,可多次 | 是,只能一次 |
| 是否提供实现 | 否 | 是 |
| 编译器作用 | 让编译通过 | 真正创建实体 |
| 常见位置 | 头文件(.h) | 源文件(.c) |
| 关键字 | extern | 无 / 具体实现 |
1.3.3 默认形参
1.3.3.1 默认形参
默认形参(Default Parameters)是C++中允许你在函数声明或定义时为形参提供默认值的一种机制。这样,当调用函数时,如果没有提供相应的实参,编译器会自动使用默认值。
默认形参可以让你编写更灵活的函数,减少函数重载的数量,并提高代码的可读性和可维护性。
1.3.3.2 声明和定义
在函数声明或定义时,为形参提供默认值。默认值从右到左依次指定,且一旦某个参数指定了默认值,其右边的所有参数都必须指定默认值。
#include <iostream>
using namespace std;
// 函数声明,提供默认形参
void printMessage(string message = "Hello, World!");
int main() {
// 调用函数时不提供实参,使用默认值
printMessage();
// 调用函数时提供实参
printMessage("Hello, Fitten Code!");
return 0;
}
// 函数定义
void printMessage(string message) {
cout << message << endl;
}
1.3.3.3 默认形参的规则
从右到左指定默认值:
必须从右到左依次为形参提供默认值。
例如:void func(int a, int b = 10, int c = 20); 是合法的,但 void func(int a = 10, int b, int c = 20); 是非法的。
可以在函数声明或定义中提供默认值:
通常在"函数声明"中提供默认值,这样调用函数时可以明确看到默认参数的值。
#include<iostream>
using namespace std;
// 函数声明
void func(int a, int b = 10, int c = 20);
// 函数定义
void func(int a, int b, int c) {
cout << "a: " << a << ", b: " << b << ", c: " << c << endl;
}
int main()
{
func(5);
return 0;
}
可以在类(后面会讲到)的成员函数声明中提供默认值:
通常在函数声明中提供默认值,这样调用函数时可以明确看到默认参数的值。
#include<iostream>
using namespace std;
class MyClass {
public:
void myFunction(int a, int b = 10, int c = 20);
};
// 类外部的实现
void MyClass::myFunction(int a, int b, int c) {
cout << "a: " << a << ", b: " << b << ", c: " << c << endl;
}
int main()
{
MyClass s1;
s1.myFunction(5);
return 0;
}
1.3.3.4 默认形参和函数重载
使用默认形参可以替代一些函数重载,使代码更简洁。
例如........什么你不知道什么是重载函数(c++特有的)?
1.3.3.4.1 函数重载
1.3.3.3.1.1 概念
函数重载:在同一个作用域中,函数名相同,但参数不同
1.3.3.4.1.2 核心规则
参数列表必须不同(以下至少满足一项):
1.参数类型不同(如 int 和 double)。
2.参数数量不同。
3.参数顺序不同(但实际类型需不同,例如 int, double 和 double, int)。
4.返回值类型不同不能构成重载(仅返回值不同会编译报错)。
5.作用域必须相同(例如同一个类或全局作用域)。
int add(int a, int b);
double add(double a, double b);
调用时,编译器会根据参数类型自动选择正确的函数。
为什么 C 没有函数重载?
C 里函数名必须唯一:
int add(int a, int b);
double add(double a, double b); // ❌ 编译错误
所以 C 只能靠:
- 不同函数名:
add_int、add_double - 或宏(危险)
👉 C++ 为了解决这个痛点,引入函数重载
C++ 如何区分同名函数?(重点)
靠的是:
函数名 + 参数列表(类型、个数、顺序)
#include <iostream>
using namespace std;
// 重载函数:参数数量不同
void print(int a) {
cout << "int: " << a << endl;
}
void print(double a) {
cout << "double: " << a << endl;
}
void print(int a, int b) {
cout << "two ints: " << a << ", " << b << endl;
}
int main() {
print(10); // 调用 void print(int a)
print(3.14); // 调用 void print(double a)
print(1, 2); // 调用 void print(int a, int b)
return 0;
}
/*重载解析流程
当调用重载函数时,编译器按以下顺序匹配:
精确匹配(参数类型完全一致)。
隐式类型转换(如 int → double)。
模板函数(如果没有更匹配的非模板函数)。
报错(如果无法匹配)。
*/
1.3.3.4.2 默认形参替代函数重载
默认形参 = 给函数“没填的参数”自动补值,所以在“参数个数不同”的情况下,它可以少写几个重载函数
注意!
👉 默认形参只能替代“参数个数不同”的重载
👉 替代不了“参数类型不同”的重载
#include <iostream>
using namespace std;
// 函数声明,提供默认形参
void printDetails(string name, int age = 18, string city = "Unknown");
int main() {
// 调用函数时不提供任何实参
printDetails("Alice");
// 调用函数时提供部分实参
printDetails("Bob", 25);
// 调用函数时提供所有实参
printDetails("Charlie", 30, "Beijing");
return 0;
}
// 函数定义
void printDetails(string name, int age, string city) {
cout << "Name: " << name << ", Age: " << age << ", City: " << city << endl;
}
/*注意事项
默认值只能在声明或定义中指定一次:
不能在函数声明和定义中重复指定默认值。
例如:
void func(int a, int b = 10); // 声明中指定默认值
void func(int a, int b = 10) { // 定义中不能再指定默认值
cout << a << ", " << b << endl;
}
正确的做法是
void func(int a,int b)
}
cout << a << ", " << b << endl;
}
*/
1.3.3.4.2.1 生活例子
❌ 没有默认形参(只能靠重载)
order();
order("微辣");
order("微辣", "大杯");
你得写 3 个函数:
void order()
void order(string spicy)
void order(string spicy, string size)
有默认形参(一个函数全搞定)
void order(string spicy = "不辣", string size = "中杯")
order(); // 不辣 + 中杯
order("微辣"); // 微辣 + 中杯
order("微辣", "大杯"); // 微辣 + 大杯
👉 一个函数 = 三个重载
但!默认形参不是万能的(重点)
❌ 它不能替代“类型不同”的重载
void print(int x);
void print(string x);
你不能写成
void print(auto x = ???); // 不存在
类型不一样,必须重载
❌ 默认形参 + 重载 = 容易翻车
危险例子(非常经典)
void foo(int x);
void foo(int x, int y = 10);
foo(5); // 💥 编译器:我该用哪个?
👉 二义性错误
1.4 c++的命名空间
[C++命名空间(名字空间)详解 - C语言中文网][(https://c.biancheng.net/view/2192.html)
1.4.1 相关概念
1.1.1 命名空间的概念
- 命名空间就是一个“容器”,用来 管理名字(变量名、函数名、类名),防止不同地方起了相同名字导致冲突。
- 作用:避免 命名冲突,尤其是当程序很大或者使用了很多库时。
- 可以理解为 “一个名字的文件夹”,名字相同的变量放在不同文件夹里不会冲突。
有时候使用命名空间具有一定的好处,
比如说你使用c++特定的输出cout和cin,他们的原型都是std::cout,std::cin。每次都写这个玩意都很麻烦,他们都包含在std这个名字空间里面,我们在声明区域就能够实现这个省略。
1.4.1.2 语法
namespace MySpace { // 定义命名空间
int a = 10;
void printA() {
std::cout << "a = " << a << std::endl;
}
}
- 这里
MySpace是命名空间的名字。 - 里面的变量
a和函数printA()都属于MySpace。
1.4.1-3 使用命名空间的方式
方法1:限定名访问
#include <iostream>
using namespace std;
namespace MySpace {
int a = 10;
void printA() {
cout << "a = " << a << endl;
}
}
int main() {
MySpace::printA(); // 用命名空间限定符访问
cout << "a = " << MySpace::a << endl;
return 0;
}
输出:
a = 10
a = 10
方法2:使用 using 声明
- 可以引入命名空间里的名字,直接使用,不用写
::。
using MySpace::a; // 只引入 a
using MySpace::printA; // 引入 printA
int main() {
printA(); // 直接使用
cout << a << endl; // 直接使用
return 0;
}
方法3:使用 using namespace
- 引入整个命名空间,所有名字都可以直接使用,但可能引入冲突。
using namespace MySpace;
int main() {
printA(); // 直接用
cout << a << endl;
return 0;
}
⚠️ 注意:大型项目一般 不要随意
using namespace std;,容易和自己定义的名字冲突。
1.4.1.4 示例:避免命名冲突
#include <iostream>
using namespace std;
namespace Space1 {
int value = 100;
}
namespace Space2 {
int value = 200;
}
int main() {
cout << Space1::value << endl; // 100
cout << Space2::value << endl; // 200
return 0;
}
- 同名变量
value在不同命名空间里不冲突。 - 这就是命名空间的核心作用。
1.4.1.5 总结
| 概念 | 说明 |
|---|---|
| namespace | 命名空间,用于管理名字,防止冲突 |
| 访问方式 | 命名空间名::名字 或 using 引入 |
| 作用 | 避免全局变量、函数、类重名,特别是大型项目或引用库时 |
| 注意事项 | 小心使用 using namespace std;,大项目容易冲突 |
🔹 类比:命名空间就像“文件夹”,文件夹里可以有同名文件,不同文件夹互不干扰。
命名空间 不是函数,它和函数是完全不同的概念。我们来区分一下:
1.4.2 命名空间和函数
1.4.2.1 命名空间 vs 函数
| 特性 | 命名空间(namespace) | 函数(function) |
|---|---|---|
| 作用 | 用来管理名字,防止变量、函数、类等冲突 | 用来执行特定操作或计算,封装可重复使用的代码 |
| 是否执行 | 只是一个“容器”,不会被调用执行 | 会被调用执行,完成任务 |
| 语法 | namespace Name { ... } |
返回类型 函数名(参数) { ... } |
| 内容 | 变量、函数、类等都可以放进去 | 一般包含可执行语句(代码块) |
| 访问 | Name::变量名 或 using |
直接用 函数名() 调用 |
1.4.2.2 核心区别
- 命名空间 = “名字的文件夹”,只是组织代码,不会运行。
- 函数 = “可执行的动作”,必须调用才能执行。
1.4.2.3 举例对比
#include <iostream>
using namespace std;//这也是命名空间哈,不然就得写std::cout了
// 命名空间
namespace MySpace {
int a = 10; // 变量
void show() { // 函数也可以放在命名空间里
cout << "Hello from MySpace" << endl;
}
}
// 普通函数
void sayHello() {
cout << "Hello from function" << endl;
}
int main() {
// 调用命名空间里的函数
MySpace::show();
// 调用普通函数
sayHello();
// 访问命名空间里的变量
cout << MySpace::a << endl;
return 0;
}
输出:
Hello from MySpace
Hello from function
10
MySpace只是一个容器,它里面的函数和变量可以访问,但命名空间本身 不会被调用执行。sayHello()是函数,需要调用才会执行。
💡 比喻:
- 命名空间 = “房间”,房间里放家具(变量)、工具(函数)。
- 函数 = “机器人”,你按下开关(调用函数),它开始工作。
只需要留意using namespace std;,它声明了命名空间 std,后续如果有未指定命名空间的符号,那么默认使用 std,代码中的 string、cin、cout 都位于命名空间 std。
1.4.3.命名空间和函数重载
命名空间:解决“名字冲突”
函数重载:解决“同一功能的不同用法”
命名空间(namespace)
技术说法
命名空间是用来把名字分组、隔离,避免同名冲突的机制。
人话
给名字加“姓氏 / 地址”
namespace A {
void print() {
cout << "A 的 print" << endl;
}
}
namespace B {
void print() {
cout << "B 的 print" << endl;
}
}
A::print();
B::print();
例子
🧑 同名不同人
- 公司里有两个「张伟」
- 一个是 技术部的张伟
- 一个是 财务部的张伟
👉 你说「张伟过来一下」——全办公室懵了
于是你改成:
- 技术部::张伟
- 财务部::张伟
📌 这就是 命名空间
命名空间解决的是什么问题?
“这个名字到底是谁?”
- 防止库与库冲突
- 防止自己和标准库冲突
- 提升大型项目可维护性
函数重载(function overloading)
技术说法
同一作用域内,函数名相同,但参数列表不同。
人话
同一个名字,根据你“怎么用”,自动选不同版本
void print(int x) {
cout << "int: " << x << endl;
}
void print(string x) {
cout << "string: " << x << endl;
}
print(10);
print("hello");
生活例子
📱 打电话这个动作
你说:「打电话」
- 打给朋友 → 手机
- 打给前台 → 座机
- 打国际 → 加区号
👉 动作名字一样,但“参数不同”,执行方式不同
📌 这就是 函数重载
函数重载解决的是什么问题?
“我想做同一件事,但输入不一样”
- 提高可读性
- 减少记忆成本
- 统一接口
把两者放在一起对比(核心)
| 对比点 | 命名空间 | 函数重载 |
|---|---|---|
| 解决的问题 | 名字冲突 | 用法不同 |
| 关注点 | 名字属于谁 | 怎么用这个名字 |
| 是否改变行为 | ❌ 不改变 | ✅ 会改变 |
| 是否同一个作用域 | ❌ 不同 | ✅ 同一个 |
| 生活类比 | 姓氏 / 部门 | 同一动作不同方式 |
一个“生活+代码”综合例子(你一眼就懂)
🏦 银行场景
命名空间 = 不同银行
namespace ICBC {
void pay(int money);
}
namespace ABC {
void pay(int money);
}
👉 ICBC::pay 和 ABC::pay 不是一回事
函数重载 = 同一家银行的不同付款方式
namespace ICBC {
void pay(int money); // 现金
void pay(int money, string card); // 刷卡
}
👉 同一银行,同一个 pay,不同参数
六、非常关键的一点(容易混)
❌ 命名空间 ≠ 函数重载
A::print();
B::print();
❗这 不是 重载
👉 因为它们 不在同一作用域
✅ 真正的函数重载
void print(int);
void print(double);
1.5.作用域
任何一种编程中,作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。C 语言中有三个地方可以声明变量:
1.在函数或块内部的局部变量。
2.在所有函数外部的全局变量。
3.在形式参数的函数参数定义中。
1.5.1 概念:
1.5.1.1 代码块作用域(block scope)
(1)含义:
代码块作用域指的是在程序代码中,由花括号 {} 包围的区域所定义的变量和函数的可见性和生命周期。
在C语言中,代码块可以是函数体、循环体、条件语句体等。
(2)特点:
局部性:变量只在声明它的代码块内有效。
生命周期:变量在代码块执行开始时创建,在代码块执行完毕后销毁。
隐藏性:变量不会在代码块外部被访问。
例子
function myFunction() {
var localVar = "我是局部变量";
console.log(localVar); // 输出: 我是局部变量
}
console.log(localVar); // 报错: localVar is not defined
在上面的例子中,localVar 是在 myFunction 函数内部定义的局部变量,只能在函数内部访问。
在函数外部尝试访问 localVar 会导致错误。
(3)与全局作用域的区别:
全局作用域:在函数外部定义的变量或函数属于全局作用域,可以在整个程序的任何地方访问。
函数作用域:在函数内部定义的变量或函数属于函数作用域,只能在函数内部访问。
1.5.1.2 文件作用域(file scope)
(1)含义:文件作用域指的是在文件(或模块)中定义的变量或函数,可以在整个文件中访问,但在其他文件中不可见。
这种作用域通常用于模块化编程,避免全局污染。
作用范围是从它们的声明位置开始,到文件的结尾处都是可以访问的。
另外,函数名也具有文件作用域,因为函数名本身也是在代码块之外
(2)特点:变量或函数在文件(或模块)的顶层定义。
可以在整个文件中访问,但在其他文件中不可见(除非显式导出)。
有助于模块化设计和代码组织
1.5.1.3 原型作用域(prototype scope)
(1)含义:原型作用域只适用于那些在函数原型中声明的参数名。函数在声明的时候可以不写参数的名字(但参数类型是必须要写上的).
多次尝试可以发现,函数原型的参数名还可以随便写一个名字,不必与形式参数相匹配(当然,这样做毫无意义)。允许你这么做,只是因为原型作用域起了作用。
(2)特点:
对象可以通过原型链访问其原型对象上的属性和方法。
如果对象本身没有某个属性或方法,会沿着原型链向上查找。
1.5.1.4 函数作用域(function scope)
(1)含义:函数作用域是指在函数内部定义的变量或函数,其可见性和生命周期仅限于该函数内部。
(2)特点:
局部变量:在函数内部声明的变量(使用 var、let 或 const)只能在函数内部访问,外部无法访问。
生命周期:函数执行完毕后,局部变量会被销毁,释放内存。
嵌套函数:在函数内部定义的函数(嵌套函数)可以访问外部函数的变量,但外部函数无法访问嵌套函数的变量。
函数作用域确保了变量的封装性,避免了命名冲突,并优化了内存管理
例子:
function myFunction() {
let x = 10; // 局部变量
console.log(x); // 可以访问
}
myFunction();
console.log(x); // 报错,x 未定义
1.5.2 解释(不想看长串的看这里,大白话解释)
1.5.2 核心概念
作用域 = 变量或函数在哪一块代码里能被使用 / 谁能看见它
换句话说:写在哪里,能用多久,就决定了它的作用域
1.5.2.1 代码块作用域(block scope)
- 通俗理解:
{}就像一个小房间,里面定义的变量只能在里面用,出了房间就“消失”。
#include <stdio.h>
int main() {
{
int a = 10; // 代码块作用域变量
printf("a = %d\n", a); // ✅ 可以访问
}
// printf("%d\n", a); // ❌ 错误,出了块外就访问不到
return 0;
}
- 口诀:
“出生在括号里,死在括号外”
1.5.2.2 函数作用域(function scope)
-
通俗理解:
函数本身就是一个大房间,函数里的变量只能在函数里用,函数执行完就消失。
#include <stdio.h>
void myFunction() {
int x = 10; // 局部变量
printf("x = %d\n", x); // ✅ 可以访问
}
int main() {
myFunction();
// printf("%d\n", x); // ❌ 错误,函数外访问不到
return 0;
}
- 口诀:
“函数作用域 = 函数内部能用,外部不能用”
1.5.2.3 全局 / 文件作用域(file scope)
- 通俗理解:
写在函数外面的变量,整个文件都能用。
#include <stdio.h>
int g = 100; // 文件作用域 / 全局变量
void printGlobal() {
printf("g = %d\n", g); // ✅ 可以访问
}
int main() {
printf("g = %d\n", g); // ✅ 可以访问
printGlobal();
return 0;
}
- 口诀:
“写在外面,大家都能用(当前文件内)”
1.5.2.4 函数参数作用域
- 通俗理解:
函数参数就像函数里的局部变量,只能在函数内部用,函数执行完就消失。
#include <stdio.h>
void add(int a, int b) { // 函数参数
printf("sum = %d\n", a + b); // ✅ 只能在函数内部使用
}
int main() {
add(5, 3);
// printf("%d\n", a); // ❌ 错误,函数外访问不到
return 0;
}
- 口诀:
“函数参数 = 局部变量,函数内能用”
| 作用域类型 | 小白理解 | 生命周期 | 可见范围 |
|---|---|---|---|
| 代码块作用域 | {} 内定义的变量 |
进入块就创建,出块就销毁 | 只在块内可用 |
| 函数作用域 | 函数里的变量 | 函数开始创建,结束销毁 | 函数内部可用,外部不可用 |
| 全局 / 文件作用域 | 函数外定义的变量 | 程序开始到程序结束 | 当前文件中任何位置可用 |
| 函数参数 | 函数里的参数 | 函数开始到结束 | 只能在函数内用 |
1.6.局部变量和全局变量
| 特性 | 局部变量 | 全局变量 |
|---|---|---|
| 定义位置 | 定义在函数或代码块内部 | 定义在所有函数外部,通常在文件顶部 |
| 作用域 | 只能在定义它的函数或代码块内使用 | 在整个文件中(甚至跨文件通过 extern)都可以使用 |
| 生命周期 | 函数调用时创建,函数结束时销毁 | 程序运行时创建,程序结束时销毁 |
| 初始值 | 如果不初始化,值是 不确定的(垃圾值) | 如果不初始化,默认初始化为 0(整型)或NULL(指针) |
| 内存位置 | 栈(stack)<底层知识> | 数据段(data segment)或全局存储区<底层知识> |
| 命名冲突 | 不会影响其他函数的同名变量 | 全局变量容易与其他文件或函数中的变量冲突,需要小心命名 |
| 访问方式 | 仅在定义的函数内部访问 | 可以在整个程序中访问,如果使用 extern 可以跨文件访问 |
#include <stdio.h>
// 全局变量
int globalVar = 10;
void func() {
// 局部变量
int localVar = 5;
printf("局部变量 localVar = %d\n", localVar);
printf("全局变量 globalVar = %d\n", globalVar);
}
int main() {
func();
// printf("%d", localVar); // 错误,localVar 在这里不可见
printf("全局变量 main中访问 globalVar = %d\n", globalVar);
return 0;
}
局部变量短命、只在局部使用;全局变量长寿、可在整个程序中使用。
Q:为什么不要使用大量的全局变量?
因为大量使用全局变量会对程序结构产生不良的影响,而且可能导致程序中各个函数之间具有太多的数据联系(在模块化程序设计的指导下,我们应该尽量设计内聚性强,耦合性弱的模块。
也就是要求你函数的功能要尽量单一,与其他函数的相互影响尽可能地少,而大量使用全局变量恰好背道而驰)
1.7.c++类和对象到底是什么意思(重要!)
看这个文章比较好
大白话解释的话
1.7.1 类(Class)——抽象的模板
类就是“蓝图”或“模板”,它定义了一类事物的 属性(成员变量) 和 行为(成员函数),但它本身不是具体的东西。
比喻:
- 类就像一份房子的设计图纸,画出了房子的结构(几间房、几扇窗、几层楼)。
- 设计图本身不能住人,它只是描述了房子应该长什么样。
C++ 示例:
#include <iostream>
using namespace std;
// 定义一个类——Car(汽车)
class Car {
public:
// 成员变量(属性)
string brand;
int year;
// 成员函数(行为)
void start() {
cout << brand << " 启动了!" << endl;
}
};
这里的 Car 就是一个 类,它描述了汽车有 brand、year,并且可以 start()。
注意:这个类只是“模板”,还没有一辆真正的车。
1.7.2 对象(Object)——具体实例
对象就是类的“实例”或“实体”,它是根据类的蓝图创建出来的具体东西。每个对象都有自己的数据(属性),并且可以调用类的方法。
比喻:
- 如果类是设计图,那么对象就是盖好的房子。
- 你可以建很多房子(对象),它们都是根据同一张图(类)建的,但房子的家具、颜色可以不同。
C++ 示例:
int main() {
// 创建对象
Car car1;
Car car2;
// 给对象属性赋值
car1.brand = "Toyota";
car1.year = 2020;
car2.brand = "Honda";
car2.year = 2022;
// 调用对象的方法
car1.start(); // 输出:Toyota 启动了!
car2.start(); // 输出:Honda 启动了!
return 0;
}
✅ 这里:
car1和car2是 对象(具体的汽车)。- 它们的数据可以不同,但行为(
start())是同一个类定义的。
3️⃣ 核心总结
| 概念 | 含义 | 举例 |
|---|---|---|
| 类 | 描述事物的模板/蓝图 | 汽车的设计图 |
| 对象 | 类的具体实例/实体 | 根据设计图建的实际汽车 |
| 成员变量 | 对象的属性 | 汽车的品牌、年份 |
| 成员函数 | 对象的行为 | 启动汽车、刹车 |
一句话记忆:类是模板,对象是实体。
学长学长,之前的c和c++打印的内容,发现了c++在打印的时候都要写std::,有没有更加简单的方法呢
有的有的,兄弟有的。
这就是我们接下来要介绍的东西。
1.8.复习区域
1.8.1 字符和字符串
1.8.1.1 概念
- 字符(char):存储单个字符,占1字节。
- 字符串:字符数组,以
\0结尾。 - C++中也可用
char[]或string类型。
#include <iostream>
using namespace std;
int main() {
// 单个字符
char ch = 'A';
cout << "ch = " << ch << endl;
// 字符串(字符数组)
char str[] = "Hello"; // 自动在末尾加 '\0'
cout << "str = " << str << endl;
// 修改字符串中的字符
str[0] = 'h';
cout << "修改后 str = " << str << endl;
return 0;
}
输出:
ch = A
str = Hello
修改后 str = hello
🔹 注:C++里推荐用
string类型,更方便:string是c++的特性,可以动态定义一个字符串,也就是说string可以根据输入的字符串分配内存空间
string s = "Hello";
s[0] = 'h';
cout << s << endl; // hello
1.8.2 数组
1.8.2.1 概念
- 数组是一组相同类型数据的集合,连续存储。
- 下标从 0 开始。
- 可以是单维或多维。
#include <iostream>
using namespace std;
int main() {
// 单维数组
int arr[5] = {1, 2, 3, 4, 5};
cout << "arr[2] = " << arr[2] << endl; // 访问第3个元素
// 多维数组(2行3列)
int matrix[2][3] = {{1,2,3},{4,5,6}};
cout << "matrix[1][2] = " << matrix[1][2] << endl; // 输出6
return 0;
}
1.8.3 循环
1.8.3.1 概念
- 循环用于重复执行一段代码。
- C/C++有三种基本循环:
for循环(已知循环次数)while循环(先判断条件)do.while循环(先执行一次再判断)
#include <iostream>
using namespace std;
int main() {
// for循环
cout << "for循环输出1~5: ";
for(int i=1; i<=5; i++)
cout << i << " ";
cout << endl;
// while循环
int j = 1;
cout << "while循环输出1~5: ";
while(j <= 5) {
cout << j << " ";
j++;
}
cout << endl;
// do-while循环
int k = 1;
cout << "do-while循环输出1~5: ";
do {
cout << k << " ";
k++;
} while(k <= 5);
cout << endl;
return 0;
}
输出:
for循环输出1~5: 1 2 3 4 5
while循环输出1~5: 1 2 3 4 5
do-while循环输出1~5: 1 2 3 4 5
1.8.4 字符串 + 循环结合示例
- 遍历字符串
#include <iostream>
using namespace std;
int main() {
char str[] = "Hello";
cout << "遍历字符串: ";
for(int i=0; str[i] != '\0'; i++) { // 遍历直到 '\0'
cout << str[i] << " ";
}
cout << endl;
return 0;
}
输出:
遍历字符串: H e l l o
1.8.5 总结概念
| 概念 | 说明 |
|---|---|
| char | 存储单个字符 |
| 字符串 | 字符数组,以 \0 结尾 |
| 数组 | 存储相同类型元素的连续内存 |
| for循环 | 已知循环次数,常用 |
| while循环 | 条件先判断,可能一次都不执行 |
| do-while循环 | 条件后判断,至少执行一次 |
| 遍历字符串 | 用循环 + \0 结束条件 |
2.指针(c语言内容)
指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,您必须在使用指针存储其他变量地址之前,对其进行声明。指针也占据字节大小, 需要自行了解
指针变量声明的一般形式为:
type *var-name;
在这里,type 是指针的基类型,它必须是一个有效的 C++ 数据类型,var-name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是指针。以下是有效的指针声明:
int *ip; /* 一个整型的指针 */
double *dp; /* 一个 double 型的指针 */
float *fp; /* 一个浮点型的指针 */
char *ch; /* 一个字符型的指针*/
1️⃣ 生活类比:指针就像“房子的门牌号”
想象:
x是你家里的 房子(变量,里面存东西)p是 门牌号(指针,告诉你房子在哪)
int x = 42; // 房子里放着 42
int* p = &x; // p 是 x 的门牌号
x= 房子里的东西(42)&x= 房子的地址(门牌号)p= 你手里的门牌号*p= 打开门牌号对应的房子,看里面的东西(解引用)
2️⃣ 类比操作
*p = 100; // 打开门,把房子里的东西改成 100
生活类比:
你有门牌号,就可以打开房子改东西
不用直接站在房子旁边
3️⃣ 指针的动态性
你可以换门牌号,让它指向另一个房子:
int y = 200;
p = &y; // p 现在指向房子 y
*p = 300; // 改变 y 里的东西
类比:
p 不固定在一个房子上,可以随时指向别的房子
Q:为什么需要指针?
2.1指针的核心优势
1️⃣ 间接访问(Indirect Access)
指针可以让你通过地址访问变量,这在很多场景非常有用。
int x = 10;
int* p = &x;
*p = 20; // 间接修改 x
- 优点:函数可以修改外部变量
- 场景:函数参数传递中,如果不想拷贝大对象,可以传指针
2️⃣ 动态内存管理(Dynamic Memory)
指针可以指向动态分配的内存,大小在运行时确定。
int* arr = new int[n]; // n 运行时确定
...
delete[] arr;
- 优点:灵活使用内存,不受编译时限制
- 场景:大数据、可变数组、图结构等
3️⃣ 高效操作数组 / 字符串 / 缓冲区
int arr[100];
int* p = arr;
p += 10; // 直接跳到 arr[10]
- 指针可以直接算地址,速度比下标访问快
- 优点:高性能控制
- 场景:系统底层、网络缓冲区、文件读写
4️⃣ 支持复杂数据结构(链表 / 树 / 图)<后面会学数据结构的>
- 链表:
struct Node {
int val;
Node* next;
};
- 树 / 图也依赖指针或智能指针
- 优点:可以动态构建任意结构
5️⃣ 函数指针(Callback / 多态前身)
void (*func_ptr)(int) = &myFunction;
func_ptr(10);
- 指针可以指向函数
- 优点:实现回调、策略模式、插件机制
6️⃣ 节省内存拷贝
void process(std::vector<int>* v);
- 传指针而不是整个数组或对象
- 优点:避免大对象拷贝,提高性能
二、指针的本质优势总结
| 优点 | 说明 |
|---|---|
| 间接访问 | 可以通过地址修改或读取变量 |
| 动态内存分配 | 内存大小运行时确定 |
| 高效操作数组/缓冲区 | 直接操作内存地址,速度快 |
| 支持复杂数据结构 | 链表、树、图等结构依赖指针 |
| 函数指针 | 实现回调、策略模式 |
| 节省拷贝 | 传递指针避免复制大对象 |
2.2 示例代码
//比大小并按照从小到大的顺序进行交换
#include<iostream>
using namespace std;
{
int a, b, c, t;
int*pa, *pb, *pc;//指针
printf("请输入三个数:");
cin>>a>>b>>c;
pa = &a;//指针pa存放变量a的地址
pb = &b;
pc = &c;
if (a > b)
{
t = *pa;
*pa = *pb;
*pb = t;
}
if (a > c)
{
t = *pa;
*pa = *pc;
*pc = t;
}
if (b > c)
{
t = *pb;
*pb = *pc;
*pc = t;
}
printf("%d <= %d <= %d\n", *pa, *pb, *pc);
//cout << *pa << " <= " << *pb << " <= " << *pc;
}
2.3 注意
1. 指针必须有明确的类型
指针的类型要和它指向的数据类型一致。
int a = 10;
int* p = &a; // 正确
👉 这里 p 是 int 类型的指针,只能存放 int 变量的地址。
如果类型不匹配:
double b = 3.14;
int* p = &b; // ❌ 错误(或强制转换后危险)
⚠️ 类型不匹配会导致:
- 访问内存错误
- 数据解释错误
- 程序崩溃
👉 规则:指针类型 = 所指向变量的类型
2. 指针一定要初始化(非常重要)<野指针>
很多新手最容易犯的错误:
int* p; // 野指针!
*p = 10; // ❌ 危险操作
这叫 野指针 —— 指向未知地址。
正确做法:
方法1:指向已有变量
int a = 10;
int* p = &a;
方法2:初始化为 NULL
int* p = NULL;
👉 这样至少不会误访问垃圾内存。
3. 区分 * 是类型的一部分,不是变量名的一部分
很多人误解这一点:
int* p, q;
实际上:
p是指针q是普通 int 变量
不是两个指针!
推荐写法(更清晰):
int *p;
int *q;
👉 每个指针单独写一行,减少歧义。
4. 注意指针的作用域和生命周期
不要返回局部变量的地址:
int* func() {
int a = 10;
return &a; // ❌ 危险
}
因为:
a是局部变量- 函数结束后内存被释放
- 返回的指针变成野指针
👉 这是 悬空指针(dangling pointer)指针必须指向有效内存
合法来源包括:
- 变量地址
- 数组
- 动态分配内存
- NULL
不要随便写:
int* p = (int*)1000; // ❌ 非法访问
这叫非法内存访问,可能直接崩溃。
这里有一个测试
第二章指针-网页代码动画
下载链接:
[源代码/网页代码动画/2.指针/指针 (2).html · 工程部Teddy Bear/c++学习 - 码云 - 开源中国](https://gitee.com/ASUS_HACKED/cpp-learning/blob/master/源代码/网页代码动画/2.指针/指针 (2).html)
这里的代码需要你复制到txt文本,然后修改后缀为html就可以用浏览器渲染啦

3.指针和数组
3.1概念
1.虽然数组和指针关系密切,好基友。但是!数组绝对不是指针,它们只是哥俩好而已。
2.数组名是数组的第一个元素的地址,也就是数组的首地址
一、什么叫「第一个元素的地址」?
先看最简单的数组:
int a[3] = {10, 20, 30};
内存里真实长这样(假设):
地址 内容
0x1000 a[0] = 10
0x1004 a[1] = 20
0x1008 a[2] = 30
那:
&a[0] == 0x1000
👉 这就是「第一个元素的地址」
二、那「数组名 a」是什么?
a
在表达式中,编译器会把 a 当成:
&a[0]
也就是说:
a == &a[0]
📌 注意:
不是“长得像”,而是数值上完全相等
那为什么还要说「数组的首地址」?
因为数组有 两种“地址”,这是很多人懵的地方。
1️⃣ 第一个元素的地址(常用)
&a[0] → int *
2️⃣ 整个数组的地址(很少提)
&a → int (*)[3]
它们:
- 值一样
- 类型不同
- 含义不同
来看区别 👇
a 和 &a 的关键区别(灵魂点)
int a[3];
| 表达式 | 地址值 | 类型 | 含义 |
|---|---|---|---|
a |
0x1000 | int * |
指向第一个元素 |
&a[0] |
0x1000 | int * |
第一个元素地址 |
&a |
0x1000 | int (*)[3] |
整个数组的地址 |
看指针运算差别:
a + 1 // 跳过 1 个 int → 0x1004
&a + 1 // 跳过整个数组 → 0x100C
用一句「人话」重写那句话
原句有点容易误导,我们换成更精确的版本:
数组名在表达式中,代表一个指向数组第一个元素的指针,它的值等于
&a[0]
或者更口语点 👇
数组名站在代码里时,看起来像数组,实际上拿出来用时就是第一个元素的地址
3.2程序例子
程序1 获取字符串的长度
#include <iostream>
#include <cstdio>//调用c的函数库
#define MAX 1024
int main()
{
char str1[MAX];
std::cout << "请输入一段字符串:";
fgets(str1, MAX, stdin);//fgets是c语言特有的,使用特有的C语言函数的时候需要在头文件加上对应的函数库,cstdio
//std::getline(std::cin, str)这个是C++的特有,包含在string库中,和fgets函数作用一样
char* target = str1;
int length = 0;
while (*target++ != '\0')
{
length++;
}
// fgets 会把 '\n' 也读进来,所以减 1
std::cout << "你输入了 " << length - 1 << " 个字符" << std::endl;
return 0;
}
#include<stdio.h>
#define MAX 1024
//程序1
//初级统计字符(无法统计中文,遇到中文报错)
int main()
{
char str1[MAX];
printf("请输入一段字符串:");
fgets(str1, MAX, stdin);
/*
fgets 函数用于从指定文件中读取字符串
函数原型:
#include <stdio.h>
...
char *fgets(char *s, int size, FILE *stream);
参数解析:
参数 含义
s 字符型指针,指向用于存放读取字符串的位置
size 指定读取的字符数(包括最后自动添加的 '\0')
stream 该参数是一个 FILE 对象的指针,指定一个待操作的数据流
一般使用 fgets()搭配数组使用,stdin的含义就是从第一个字符开始读取
*/
char* target = str1;
int length = 0;
while (*target++ != '\0')
/*不能用(*str++ != '\0')的原因是因为str是一个数组名字,
数组名(也可以理解为数组就是一个固定的地址,无法进行修改,而指针是可以被修改的,指针是一个左值)是一个常量是不可被改变的,
但是++需要搭配一个可改变的变量,既需要左值(左值是一个变量,可以被修改与改变)*/
{
length++;
}
printf("你输入了%d个字符", length - 1 /*此处是因为fgets在读取的时候将'\n'一起读取了,故需要在计数器-1过滤'\n'*/);
return 0;
}
程序2 指针模拟实现strcat和strcat、strncat使用
// 全局变量、头文件区域
#include <iostream>
#include <cstring>
using namespace std;
#define MAX 1024
// 程序1
// 利用指针模拟实现 strcat 拷贝
void test1()
{
char str1[2 * MAX]; // 防止越界
char* targets1 = str1;
cout << "请输入第一个字符串:";
fgets(str1, MAX, stdin);
char str2[MAX];
char* targets2 = str2;
cout << "请输入第二个字符串:";
fgets(str2, MAX, stdin);
// 指针移动到 '\0'
while (*targets1++ != '\0');
targets1 -= 2; // 去除 fgets 带来的 '\n'
while ((*targets1++ = *targets2++) != '\0');
cout << "连接后的结果是:" << str1 << endl;
}
// 程序2
// 利用 scanf + strcat
void test2()
{
char str1[MAX];
cout << "请输入第一段字符串:";
scanf("%s", str1);
char str2[MAX];
cout << "请输入第二段字符串:";
scanf("%s", str2);
strcat(str1, str2);
cout << "连接结果:" << str1 << endl;
}
// 程序3
// fgets + strcat
void test3()
{
char str1[MAX];
cout << "请输入第一段字符串:";
fgets(str1, MAX, stdin);
for (int i = 0; i < MAX; i++)
{
if (str1[i] == '\n')
{
str1[i] = '\0';
break;
}
}
char str2[MAX];
cout << "请输入第二段字符串:";
fgets(str2, MAX, stdin);
for (int i = 0; i < MAX; i++)
{
if (str2[i] == '\n')
{
str2[i] = '\0';
break;
}
}
strcat(str1, str2);
cout << "结果为:" << str1 << endl;
}
// 程序4
// 指针模拟 strcat(含中英文)
void test4()
{
char str1[MAX];
char* targets1 = str1;
cout << "请输入第一段字符串:";
fgets(str1, MAX, stdin);
for (int i = 0; i < MAX; i++)
{
if (str1[i] == '\n')
{
str1[i] = '\0';
break;
}
}
char str2[MAX];
char* targets2 = str2;
cout << "请输入第二段字符串:";
fgets(str2, MAX, stdin);
for (int i = 0; i < MAX; i++)
{
if (str2[i] == '\n')
{
str2[i] = '\0';
break;
}
}
cout << "请输入要连接的字符数:";
int n;
cin >> n;
while (*targets1++ != '\0');
targets1 -= 1;
char ch;
while (n--)
{
ch = *targets1++ = *targets2++;
if (ch == '\0')
break;
// 处理中文(负值)
if ((int)ch < 0)
{
*targets1++ = *targets2++;
}
}
*targets1 = '\0';
cout << "连接的结果为:" << str1 << endl;
}
// 程序5
// strncat 指定字符数拷贝
void test5()
{
char str1[MAX];
cout << "请输入第一段字符串:";
fgets(str1, MAX, stdin);
for (int i = 0; i < MAX; i++)
{
if (str1[i] == '\n')
{
str1[i] = '\0';
break;
}
}
char str2[MAX];
cout << "请输入第二段字符串:";
fgets(str2, MAX, stdin);
for (int i = 0; i < MAX; i++)
{
if (str2[i] == '\n')
{
str2[i] = '\0';
break;
}
}
cout << "请输入要连接的字符串个数:";
int n;
cin >> n;
strncat(str1, str2, n);
cout << "输出结果:" << str1 << endl;
}
// 主函数
int main()
{
/*
test1 指针模拟 strcat
test2 scanf + strcat
test3 fgets + strcat
test4 指针模拟 strcat(中英文)
test5 strncat
*/
test1();
return 0;
}
程序3 指针模拟实现strcmp和strcmp、strncmp的使用
#include <iostream>
#include <string>
using namespace std;
// 程序1
// 手动模拟 strcmp(逐字符比较)
void test1()
{
string str1, str2;
cout << "请输入第一段比较字符串:";
getline(cin, str1);
cout << "请输入第二段字符串:";
getline(cin, str2);
cout << "------开始比较-------" << endl;
size_t i = 0;
while (i < str1.size() && i < str2.size())
{
if (str1[i] != str2[i])
{
cout << "两个字符串不相等,第 " << i + 1 << " 个字符不同!" << endl;
return;
}
i++;
}
if (str1.size() == str2.size())
{
cout << "两个字符串输入相等" << endl;
}
else
{
cout << "两个字符串长度不同,第 " << i + 1 << " 个字符不同!" << endl;
}
}
// 程序2
// 使用 string::compare(等价 strcmp)
void test2()
{
string str1, str2;
cout << "请输入第一段字符串:";
getline(cin, str1);
cout << "请输入第二段字符串:";
getline(cin, str2);
cout << "---开始比较----" << endl;
int result = str1.compare(str2);
if (result == 0)
{
cout << "两个输入字符串一样" << endl;
}
else
{
size_t pos = 0;
while (pos < str1.size() && pos < str2.size() && str1[pos] == str2[pos])
{
pos++;
}
cout << "两个字符串不相等,第 " << pos + 1 << " 个字符不同!" << endl;
}
}
// 程序3
// 等价 strncmp:比较前 n 个字符
void test3()
{
string str1, str2;
int n;
cout << "请输入第一个字符串:";
getline(cin, str1);
cout << "请输入第二个字符串:";
getline(cin, str2);
cout << "请输入要比较的字符个数:";
cin >> n;
cin.ignore(); // 清理换行
string sub1 = str1.substr(0, n);
string sub2 = str2.substr(0, n);
if (sub1 == sub2)
{
cout << "str1 和 str2 前 " << n << " 个字符相同!" << endl;
}
else
{
size_t i = 0;
while (i < sub1.size() && i < sub2.size() && sub1[i] == sub2[i])
{
i++;
}
cout << "两个字符串不相同,第 " << i + 1 << " 个字符出现不同" << endl;
}
}
// 程序4
// 模拟 strncmp(支持 UTF-8 字节级比较)
void test4()
{
string str1, str2;
动画演示
源代码/网页代码动画/3.指针和数组/3.指针和数组.html · 工程部Teddy Bear/c++学习 - 码云 - 开源中国
4.指针数组和数组指针
指针数组:指针数组是一个数组,每个数组元素存放一个指针变量。
数组指针:数组指针是一个指针,它指向的是一个数组。
一句话先给你结论(背这个)
[]的优先级高于\*
也就是说:
👉 先看是不是数组,再看是不是指针
4.1指针数组(array of pointers)
1.定义
int *p[3];
2.拆开读
p[3]:p 是一个 数组int *:数组里存的是 int 指针
👉 这是“指针数组”
p[0] → int*
p[1] → int*
p[2] → int*
每个元素都是一个地址。
int a = 10, b = 20, c = 30;
int *p[3] = { &a, &b, &c };
printf("%d\n", *p[1]); // 20
例如:int* p1[3]; 根据关系运算符结合性,[]结合性比* 大, 所以先结合[]再结合* 。
数组下标的优先级要比取值运算符的优先级高,所以先入为主,p1 被定义为具有 5 个元素的数组。
那么数组元素的类型呢?是整型吗?显然不是,因为还有一个星号,所以它们应该是指向整型变量的指针。也就是一个数组存放5个指向整型变量的指针
常见用途
char *argv[]- 字符串数组
- 函数指针表
- CTF / 逆向里的跳转表
4.2 数组指针(pointer to array)
定义
char (*p)[5];
⚠️ 括号是灵魂,没有它意思就全变了
拆开读
*p:p 是一个 指针(*p)[3]:它指向一个 含 3 个 int 的数组
👉 这是“数组指针”
最高层(指针) *P
|
| 指向
V
第二层(数组) 下标 0 1 2 3 4
元素 char char char char char
char temp[5] = {'a','b','c','d','e'};
char (*p)[5] = &temp;
for(int i = 0;i<5;i++)
{
printf("%c",(*p)[i]);
}
就相当于一个指针指向一个数组(这是指向整个元素),感觉是不是和之前学的'指针和数组'有点类似?
char temp[5] = {'a','b','c','d','e'};
char*p = temp;
for(int i = 0;i<5;i++)
{
printf("%c",*p++);
}
这两种方法都可以访问数组元素,但是容易观察到第二段代码是指针是指向数组的第一个元素的地址,事实上是一个指向int类型变量的指针(指向变量),而不是指向数组
数组名 = 数组第一个元素的地址 = 数组的首地址,但是他们的概念不同
第一段代码才是名正言顺的是一个指向数组的指针
两者放在一起对比(秒懂)
| 项目 | 指针数组 | 数组指针 |
|---|---|---|
| 定义 | int *p[3] |
int (*p)[3] |
| 本质 | 数组 | 指针 |
| 存的内容 | 多个指针 | 一个数组地址 |
p+1 |
跳到下一个指针 | 跳到下一个数组 |
| 常见用途 | argv / 字符串表 | 二维数组参数 |
4.3示例代码
//头文件区域,全局函数区域
#include<iostream>
#include<string.h>
//程序1
//指针数组和数组指针的混合使用
int main()
{
const char* array[5] = {"FishC","Five","Star","Good","WoW"};
const char* (*p)[5] = &array;
/*
&array是取出整个数组的地址
这里是一个指向'数组指针'的指针,<后面会提到>
结合性从左到右, 所以 p 先被定义成一个指针变量。
指向一个具有5个元素的数组,p就指向它,数组类型是 char *,即指向5个char *类型元素的数组的指针(套娃),
&array是取出整个数组的地址,而array数组又存放指针;
⚠️ 在 C++ 中:
字符串字面量类型是 const char[N],不能用 char* 指向字符串字面量,所以必须写成 const char*
最高层(指针) *P
|
| 指向
V
第二层(数组) 下标 0 1 2 3 4
这个数组又存放指针 元素 char* char* char* char* char*
| | | | | |
| 每个指针指向 | | | | |
| | | | | |
V v V V V V
第三层 "FishC" "Five" "Star" "Good" "WoW"
*/
int i, j;
for (i = 0; i < 5; i++)
{
for (j = 0; (*p)[i][j] != '\0'; j++)//在这里(*p)就是array
{
printf("%c",(*p)[i][j]);
}
printf("\n");
return 0;
}
4.4 扩展:访问数组元素的两种方法
浅浅科普一下,下标法和指针法
在C/C++语言中,下标法和指针法是两种用于访问数组元素的常见方法,它们各有特点,
4.4.1 下标法
是通过数组名和下标来访问数组元素。数组名代表数组的首地址,而下标法则是表示元素在数组中的位置偏移量
#include<iostream>
using namespace std;
int main()
{
int arr[5] = {10,20,30,40,50};
//使用下标法访问数组的元素
for(int i = 0;i < 5;i++)
{
cout<<a[i];
}
return 0;
}
4.4.2 指针法
是一个利用指针变量来访问数组元素,并通过指针的移动来访问数组中的各个元素。由于'数组名'本身就是一个指针常量(指向数组的首元素的地址),所以可以使用指针来操作数组
int main()
{
int arr[5] = {10,20,30,40,50};
int* ptr = arr;//指针prt指向数组arr的首地址
//使用指针法访问数组元素
for(int i = 0;i < 5;i++)
{
printf("*(prt + %d) = %d \n ",i,*(prt + i));
}
return 0;
动画演示
源代码/网页代码动画/4.指针数组和数组指针/指针数组和数组指针.html · 工程部Teddy Bear/c++学习 - 码云 - 开源中国
5.指针和二维数组
5.1 概念与区别
1.如果看不懂的话也可以看看这个解释
二维数组与指针详解-CSDN博客
1.其实没有真正的二维数组,人为形象规定叫二维数组比较准确,在物理层内存块来说,是一块联系的内存块存储
并不是我们直观想象中的「表格 / 矩阵」(表格只是逻辑上的形态)。
举个例子:int arr[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
- 逻辑上:它是一个 3 行 4 列的表格;
- 内存上:它是连续的 12 个
int类型数据,按照「第 0 行 → 第 1 行 → 第 2 行」的顺序依次存放(先存完第一行的 4 个元素,再存第二行,以此类推); - 核心属性:
arr是二维数组名,代表整个二维数组的首地址;arr[i]表示第i行的「一维数组名」,代表第i行的首地址;arr[i][j]表示第i行第j列的具体元素。

2.二维数组的行地址和各个元素地址的表示,数组名与指针的关系
int a[3][4]
| 表达式 | 含义 |
|---|---|
a |
数组首行地址,等价于 &a[0] |
&a |
整个二维数组的地址 |
a[i] |
第 i 行首元素的地址,等价于 &a[i][0] |
&a[i] |
第 i 行的地址(一整行) |
a[i] + j |
第 i 行第 j 个元素的地址,等价于 &a[i][j] |
a[i][j] |
第 i 行第 j 个元素 |
&a[i][j] |
第 i 行第 j 个元素的地址 |
*a |
首行首元素的地址,等价于 a[0] 或 &a[0][0] |
*(a + i) |
第 i 行首元素的地址,等价于 a[i] |
*(a + i) + j |
第 i 行第 j 个元素的地址 |
**a |
第 0 行第 0 个元素的值(a[0][0]) |
*(*(a + i) + j) |
第 i 行第 j 个元素的值 |
不理解的话就去看看上节课的科普内容<指针法和下标法>
这里有个公式可以记一下:(a+i) == a[i]; 加上个号会等于指针变量去掉号右边加个中括号,中括号内的值为指针变量所要增加的值,(a+i)+j = a[i]+j;
对于 *(*(a+i)+j) = a[i][j],我们可以先去掉最外面的*变成(*(a+i))[j],再去掉*为a[i][j]
5.2 例子
程序1
//程序1
//理解 * (解引用)的使用方法<即上节课我们提到的指针法访问数组>
void test1()
{
char array[2][3][5] = {
{
{'A','B','C','D','E'},
{'F','G','H','I','J'},
{'K','L','N','M','O'}
},
{
{'P','Q','O','S','T'},
{'U','V','W','Y','Z'},
{'a','b','c','d','e'}
}
};
printf("*(*(*array + 1) + 2):%c\n", *(*(*array + 1) + 2));//这个这么解释可能会好一点,*(*(*(array + 0) + 1)+2)即array[0][1][2],下面的就以此类推,
printf("*(*(*(array + 1) + 1) + 2) : %c\n", *(*(*(array + 1) + 1) + 2));
printf("***array:%c\n", ***array);
printf("*(**array + 1):%c", *(**array + 1));
}
程序2
//程序2
//利用int (*)[]类型强制转换成二维数组
void test2()
{
int array[9] = { 1,2,3,4,5,6,7,8,9 };
int (*p)[3] = (int(*)[3]) & array;//int (*)[3]这个类型你要是上节课能听得懂的话,那就很容易理解,(int (*)[3]&array == *array[3]),取出array整个数组的地址,利用数组指针强制转换成二维数组
printf("%d\n", p[2][2]);
}
6.指向指针的指针
6.1 概念
指向指针的指针,顾名思义,就是一个指针,它存储了另一个指针的地址。也就是说,指向指针的指针是指向一个指针变量的指针。
解释:
- 指针是一个存储地址的变量,它指向某个数据的内存位置。
- 指向指针的指针,即二级指针,是指存储另一个指针地址的指针。
为什么需要指向指针的指针?
指向指针的指针在多级链表、动态内存管理(如双重指针传递动态数组的地址)等高级编程中很有用。
6.2 例子
程序1
void test1()
{
int num = 520;
int* p1 = #
int** p2 = &p1;
/*或许你这个有疑问,我们前面学过指针它本质上是一个地址是吧,那为什么还要取出p的指针的地址呢?你指向指针的指针不就是要一个地址吗?那 p 不就是一个地址吗?这里来个演示
int num = 520;
int* p = #
printf("*p = %p\n", p);//这里的打印的是指针 p 的"值"(注意是值,不是本身),即 num 的地址。
printf("*p = %p\n", &p);//这里才是打印指针 p 本身的地址。
printf("num = %p", &num);//这里打印的就是 num 的地址,等效于注释中第三行代码。
那这里你估计就有眉目了,
int* p1 = #//这里定义了一个指针 p ,类型是int*,它指向的"值"就是 num 的地址。
int** p2 = &p1;//这里定义了一个指向指针的指针 p2 ,它的类型就是int**,它指向的"值"就是指针 p 的地址。
扩展一下知识点你就理解了,
一级指针 (int*): 直接指向一个整数变量。
二级指针 (int**)<也叫指向指针的指针>: 指向一个一级指针,即存储一级指针的地址。
三级指针 (int***): 指向一个二级指针,即存储二级指针的地址。
*/
int*** p3 = &p2;
printf("num = p\n", &num);// 打印变量num的地址
printf("*p1 = %p\n", p1);// 打印指针p1的值(它所指向的地址)
printf("&p1 = %p\n", &p1);// 打印指针p1本身的地址
printf("**p2 = %p\n", p2);// 打印指针p2指向的地址(即p1的地址)
printf("&p2 = %p\n", &p2);// 打印指针p2本身的地址
printf("***p3 = %p\n", p3);// 打印指针p3指向的地址(即p2的地址)
printf("&p3 = %p", &p3);// 打印指针p3本身的地址
}
程序2
//程序2
//作业解析,这个其实就是回顾指针数组和数组指针的应用
void test2()
{
//初始化指针数组
const char* pArray[4] = {
"Hello!",
"HOW ARE YOU",
"Fine ,thank you .And you?",
"I'm FINE too"
};
const char* (*p)[4] = &pArray;
int i;
for (i = 0; i < 4; i++)
{
printf("%s\n", (*p)[i]);
}
}
程序3
void test3()
{
char a[4][3][2] = {
//a[0]
{
{'a','b'},{'c','d'},{'e','f'}
},
//a[1]
{
{'g','h'},{'i','j'},{'k','l'}
},
//a[2]
{
{'m','n'},{'o','p'},{'q','r'}
},
//a[3]
{
{'s','t'},{'u','v'},{'w','x'}
}
};
const char (*pa)[2] = &a[1][0];//指向一维数组{'g','h'}
const char (*ppa)[3][2] = &a[1];//指向二维数组a[1]
printf("*pa = %c\n", *(*(pa + 8) + 1));//指针法
printf("*ppa = %c", *(*(*(ppa + 2) + 2) + 1));
}
程序4
void test4()
{
char str[1024];
char* p = str;//间接访问地址
char* pos[1024] = { 0 };//初始化指针数组,用于记录每个单词的地址。
int len = 0;
printf("请输入一个英文句子:");
while ((str[len++] = getchar()) != '\n' && len + 1 < 1024);
str[len - 1] = '\0';//str[len]存放的是'\n',将其替换为'\0';思考:为什么是str[len - 1]而不是str[len]呢
int cWord = 0;//统计单词数
int i = 0, j = 0;//初始化计数器
if (*p != ' ')
{
pos[i++] = p;
cWord++;
}
int cChar = 0;//统计字符数
int max = 0;
while (len--)
{
if (*p++ == ' ')
{
//判断最大字符数
max = cChar > max ? cChar : max;
cChar = 0;
//到底了,退出循环
if (*p == '\0')
{
break;
}
//单词数+1
if (*p != ' ')
{
pos[i++] = p;
cWord++;
}
}
else //没有else会把空格计入进去
{
cChar++;
}
}
max = --cChar > max ? cChar : max;//最后会多算一个'\0',所以减去
//申请可变长数组,max+1,否则'\0'放不下
//char result[cWord][max + 1];源代码中这个地方是无法使用,是因为VS不支持直接使用可变长数组;也是得我们学习到内存分配,即malloc函数才能处理这个,我们能干的只有定义固定数组
//使用固定大小的二维数组,假设每个单词最多100个字节
char result[100][100];
//将切割好的单词放进二维数组里面
for (i = 0; i < cWord; i++)
{
for (j = 0; *(pos[i] + j) != ' ' && *(pos[i] + j) != '\0'; j++)
{
result[i][j] = *(pos[i] + j);
}
result[i][j] = '\0';
}
//打印结果
printf("分割结果已存储到result[%d][%d]的二维数组中...\n", cWord, max+1);
printf("现在依次打印每个单词:\n");
for (i = 0; i < cWord; i++)
{
printf("%s\n", result[i]);
}
}
7.指针和常量
7.1 指向常量的指针---常量指针(const int* p)
1.含义
指针指向的值是一个常量,不能通过指针修改这个值。就是实现read-only作用,只读不修改
2.作用
主要用来保护数据,防止其被意外修改。
3.使用规则
1.指针可以修改为指向不同的变量/常量<相当于一个读书卡能借不同的书,但是这个读书卡没权限修改书中的内容>
2.可以通过解引用来访问读取指针指向的数据,即可以访问。(就是有权限访问数据)
例如:
int num = 520;
const int cnum = 888;//这里是定义了一个只读的变量(说白了就是一个不可修改的常量)
const int* pc = &cnum;//现在这个指针(不可修改)指向的是一个'常量'
printf("cnum = %d,&cnum:%p\n", cnum, &cnum);
printf("*pc = %d,pc = %p\n", *pc, pc);//通过解引用访问到了cnum这个常量
pc = #//现在这个指针(不可修改)指向的是一个'变量'
printf("num = %d,&num = %p\n", num, &num);
printf("*pc = %d,pc = %p", *pc, pc);
再例如:
const int num = 520;
const int* p = # // p是一个指向常量的指针
printf("%d\n", *p); // 可以访问num的值
// *p = 1000; // 这行代码会报错,因为不能通过p修改num的值
int anotherNum = 1000;
p = &anotherNum; // p可以指向不同的变量
3.不可以通过解引用来修改指针指向的变量(也就是无权限修改数据)
因为const类型就是read-only(只读)
7.2 指针常量(int *const p)
1.含义
指针自身是常量,不能再指向其他地址。<把指针看做成一个钥匙,把它指向的变量看做保险柜,这个钥匙有对应的保险柜,保险柜放什么都可以,但是这个钥匙只能开这个保险柜>
2.作用
固定指针的指向,强制指针始终指向某个内存地址,防止意外修改指向
3.使用法则
1.指针不能修改为指向不同的变量或常量。
2.可以通过解引用来访问和修改指针指向的数据。
int num = 520;
int* const p = # // p是一个常量指针,指向num
printf("%d\n", *p); // 可以访问num的值
*p = 1000; // 可以修改num的值
// int anotherNum = 1000;
// p = &anotherNum; // 这行代码会报错,因为不能让p指向其他变量
| 类型 | 语法 | 指针指向是否可变 | 指向的数据是否可修改 |
|---|---|---|---|
| 指针常量 | int* const |
× 不可变 | √ 可修改 |
| 常量指针 | const int* |
√ 可变 | × 不可修改 |
7.3 指向指针的指针(const int** q)
指向指向常量的指针的指针(const int** q)
1.语法形式
q 是一个二级指针,指向一个const int 类型的指针,被指向的指针(const int)本身可以通过修改指向的地址,但是不能通过它修改所指的常量数据
2.关键特性
通过 q 可以修改其指向的一级指针(p)也就是可以换指向对象和指向常量的指针用法一样;但是不能通过解引用修改常量的数据,也就是不能q = 888;q只有可读权限;
3.作用
允许间接修改指向常量的指针的地址,同时保护常量的不可变性,常用于函数参数传递和多维常量数据结构操作
例如
int num = 520;
const int* p = #
const int** q = &p;
printf("%d\n", **q);
7.4 指向'指向常量指针'的常指针
特点
指针指向不可修改,指针指向的值也不可以修改<专门专用,最高级保护>
const int num = 520;
const int * const p = #
const int * const *q = &p;
注意哈,const 永远限制紧随着它的标识符。const int * const *q = &p; 相当于 (const int) * (const *q) = &p;即第一个 const 限制的是 **q 的指向,第二个 const 限制的是 *q 的指向,唯有一个漏网之鱼 —— q 没有被限制
以上就是本节课知识点O(≧▽≦)O,如果你还不知道的话就看看这个文章: https://blog.csdn.net/as480133937/article/details/120804503
7.5 例子
void work1()
{
//A:
const int numA = 520;
int* pA = &numA;
//在VS中报错类型显示"const int* 类型的值不能初始化为int* 类型的实体",说人话就是这里的类型不一致,你程序现在可能不显示那是因为我将文件类型改为了.c类型(C语言程序),我们就是推荐使用.cpp类型,因为是程序规则更加严格
// num是一个常量,其地址被分配给一个非const的指针p,这会导致尝试修改常量的风险。就是可以通过指针修改这个常量,const这个保护就没有作用啦
//B:
int numB = 520;
const int* pB = &numB;
//定义了一个指向常量的指针,符合使用规则1
//C:
const int numC = 520;
const int* pC = &numC;
//定义了一个read-only变量,同时定义了一个指向const常量的指针,符合规则。
//D:
const int numD = 520;
const int* const pD = &numD;
//定义了一个指向常量的常量指针,不可通过指针改变指针所指向的值,也不可以改变指针指向
}
8.参数和指针
本次章节好像也没什么好写的,主要是例子
例子
你应该听说过 itoa 函数(函数文档 -> 传送门),它的作用是将一个整数转换成字符串形式存储。现在要求我们自己来实现一个类似功能的函数 myitoa(int num, char *str),该函数的第一个参数是待转换的整型变量,第二参数传入一个字符指针,用于存放转换后的字符串。
程序实现的例子如下:
……
int main(void)
{
char str[10];
printf("%s\n", myitoa(520, str));
printf("%s\n", myitoa(-1234, str));
return 0;
}
请你自行复制去vs中研究一下这些例子
//程序1
//课后作业6
void func(int b[][3]);
void func(int b[][3])
/*
func函数中,func(int b[][3])定义了一个形式参数,形参大小就是指针的大小。
这里和主函数a[3][4]所定义的宽度不同,在C语言中,这种写法是合法的,编译器会重新帮你进行排序,所以按照b[][3]的写法就是b[4][3];
不过这种写法在C++这种写法是不合格的,C++规定维度统一,在C++中,数组的维度信息在编译时需要被准确地确定,以确保类型安全
也就是你调用形式参数和实际参数形式必须一样,将a[3][4]传参时,形式参数形式必须也是b[][4];
b[4][3] = {
{1,2,3},
{4,5,6},
{7,8,9},
{10,11,12}
}
*/
{
printf("sizeof(b) = %d\n", sizeof(b));
/*在函数 func(int b[][3]) 中,b 是一个二维数组的指针,具体来说,b 是一个指向包含3个整数的数组的指针。
这里的 [] 实际上是一个省略号,表示数组的行数可以是任意的,但列数必须是固定的(这里是3)。
因此,sizeof(b) 计算的是指针的大小,而不是整个二维数组的大小。
在大多数现代系统上,指针的大小通常是4字节(32位系统)或8字节(64位系统)<我们现在电脑都是64位操作系统了>。
*/
printf("%d\n", b[2][2]);
}
void test1()//我这里都是将int main替换成void test number形式,这样子你就很方便进行运行测试
{
int a[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};//在主函数里,定义了一个数组,这个数组int a[3][4]
printf("sizeof(a) = %d\n", sizeof(a));
func(a);//将主函数的实际参数传进func分支函数中,这个过程叫做传数组(传数组的首元素的地址,数组本质也是一个指针)
}
//程序2
//利用itoa到达将'整型数据'转化为'字符型'
void test2()
{
int i;
char a[33];
printf("请输入一个数字:");
scanf("%d", &i);
itoa(i, a, 10);//itoa(待转化的整型数据,将转化的结果存储到字符数组中,指定转化的进制也就是转化成什么进制给字符数组a)
/*
itoa函数解析
函数概要:
itoa 函数将整型值转换为指定进制表示的标准字符串。
如果是十进制数,并且数值为负,那么转换后的字符串前边有一个负号('-');如果是其他进制数,其值始终被认为是无符号类型。
用于存放的字符串必须拥有足够的空间,以便可以容纳任何可能的数值:对于 2 进制数,需要使用 (sizeof(int)*8+1) 个字节来存放。
比如在 16 位平台上,需要 17 个字节来存放;在 32 位平台上,则需要 33 个字节来存放。
注意:
该函数并不是标准的 C 语言函数,所以不能在所有的编译器中使用。
为了提高程序的可移植性,请使用 sprintf 函数代替。
函数原型:
#include <stdlib.h>
...
char *itoa(int value, char *str, int base);
参数解析:
参数 含义
value 待转换的整型数值
str 用于存放转换结果的字符串
base 1. 指定转换结果用多少进制数来表示;2. 取值范围是 2~36(2 表示二进制,10 表示十进制……)
返回值(说人话就是函数输出结果):
返回值是一个指向转换结果的指针,同 str 参数。
使用例子:
#include <stdio.h>
#include <stdlib.h>//包含了标准库函数的头文件,这里用来使用itoa函数
int main()
{
int i;
char buffer[33];
printf("Enter a number: ");
scanf("%d", &i);
itoa(i, buffer, 10); //将i转换为十进制字符串。
printf("decimal: %s\n", buffer);//输出十进制字符串
itoa(i, buffer, 16);//将i转换为十六进制字符串。
printf("hexadecimal: %s\n", buffer);//输出十六进制字符串
itoa(i, buffer, 2);//将i转换为二进制字符串
printf("binary: %s\n", buffer);//输出二进制字符串
return 0;
}
*/
printf("输出结果为:%s\n", a);
}
//程序3
//类itoa实现将'整型数据'转化为'字符型'(初级版本:强制转化版本)
void test3()
{
int a;
printf("请输入一个数据:");
scanf("%d", &a);
printf("a = %d\n", a);
printf("输出的结果为:%d\n", (char)a);
//printf("输出的结果为:%c\n",(char)a);#这个不能使用%c,因为这个使用c的话是字符型,输出的结果需要经过ASCII码转化,例如将a = 64,以%c的形式输出的话,对应ASCII码就是:@
}
//程序4
//类实现itoa函数实现将'字符型'转化为'整型'(sprintf函数)
void test4()
{
char buffer[256];
int i;
printf("输入一个参数:");
scanf("%d", &i);
sprintf(buffer," %d", i);
//i 是一个整数值变量,它包含了你希望转换为字符串的整数。sprintf 函数的作用是将格式化的数据写入字符串中。具体来说,"%d" 是一个格式说明符,用于指定 i 是一个十进制整数。
/*
* sprintf函数
函数概要:
与 printf 函数类似,不过 sprintf 函数是将格式化数据写入到字符串中。
缓冲区的尺寸必须足够大,以至于可以包含整个转换后的结果。(snprintf 函数是更安全的版本)
format 参数后边的额外参数数量由 format 决定,具体用法请参考 printf 函数中格式化占位符的解释。
函数原型:
#include <stdio.h>
...
int sprintf(char *str, const char *format, ...);
参数分析:
参数 含义
str 指向存放结果字符串的缓冲区(这是一个指向字符数组的指针,用于存储格式化后的字符串。该数组必须有足够的空间来容纳格式化后的字符串,包括字符串结束符 '\0')
format 格式化字符串(这是一个格式控制字符串,它指定了后续参数的输出格式。格式控制字符串中可以包含普通字符和格式说明符,格式说明符以 % 开头,用于指定后续参数的类型和输出格式,例如 %d 用于整数,%f 用于浮点数等)
... 可选参数,参考printf文档(也叫可变参数,可以传递零个或多个参数,这些参数将按照格式字符串中的说明进行格式化,并写入到 str 指向的字符数组中)
格式化字符串:
格式字符串 format 可以包含普通字符和格式说明符。格式说明符是以 % 开头,后面跟着一个或多个字符,用于指定如何格式化后续的参数
例如:
%d:用于格式化 int 类型的整数。
%f:用于格式化 float 或 double 类型的浮点数。
%s:用于格式化 char* 类型的字符串。
%c:用于格式化单个字符 char。
%x:用于格式化十六进制整数。
%p:用于格式化指针地址。
%u:用于格式化无符号整数
注意事项:
缓冲区溢出: 使用 sprintf 时需要注意目标缓冲区的大小,以避免缓冲区溢出。缓冲区溢出可能会导致程序崩溃或安全漏洞,说人话就是要在初始化字符数组的时候宽度设置大一点
*/
printf("输出结果:%s", buffer);
}
//程序5
//类实现itoa函数实现将'字符数据'转为'整型'(snprintf函数)
void test5()
{
char array[256];
int i;
printf("请输入一个字符:");
scanf("%d", &i);
snprintf(array, 10, "%d",i);
/*
snprintf函数
函数概要:
snprintf 函数是 sprintf 函数的安全版本,因为它在调用的时候需要指定缓冲区的最大尺寸,这样可以有效避免缓冲区溢出。
如果写入的字符串尺寸大于 size - 1,那么后边的字符将被丢弃,但依旧会统计进长度中(返回值)。
format 参数后边的额外参数数量由 format 决定,具体用法请参考 printf 函数中格式化占位符的解释。
函数原型:
#include <strdio.h>
...
int snprintf(char *str, size_t size, const char *format, ...);
参数解析:
参数 含义
str 指向存放结果字符串的缓冲区(指向用于存储格式化后字符串的字符数组的指针。)
size 1.限定缓冲区最大可写入的字节数;2.字符串最多可以拥有 size - 1 个字符,最后一个空位用于存放 '\0';3. size_t 被定义为无符号整型(指定 str 所指向数组的最大可写入字节数,包含字符串结束符 '\0'。如果格式化后的字符串长度超过 size - 1 个字符,多余的字符将被截断,并且会在字符串末尾添加 '\0'。)
format 格式化字符串,用于指定输出的格式,其中可以包含普通字符和格式说明符(如 %d、%s、%f 等)
... 可变参数
例子:
#include <stdio.h>
int main() {
char buffer[20];
int result;
// 使用 snprintf 格式化字符串
result = snprintf(buffer, sizeof(buffer), "The number is %d", 123);
if (result >= 0) {
if (result < sizeof(buffer)) {
printf("Formatted string: %s\n", buffer);
} else {
printf("The string was truncated. Needed %d bytes.\n", result + 1);
}
} else {
printf("An error occurred while formatting the string.\n");
}
return 0;
}
*/
printf("输出的结果是:%s", array);
}
//程序6
//类实现itoa函数实现将'字符串型'转化为'整型'<这个你肯定写过了,在编写这个的日期是2.24,这天你问了我这个158行代码含义>
void test6()
{
int ch;
int num = 0;
printf("请输入一个整型数据:");
do
{
ch = getchar();
if (ch >= '0' && ch <= '9')
{
num = 10 * num + (ch - '0');
}
else
{
if (num)
{
break;
}
}
} while (ch != '\n');
printf("输出的结果是:%d",num);
}
//程序7
// 类实现itoa函数实现将'整型'转化为'字符型(小甲鱼课后作业0)
char* myitoa(int num, char* str);
char* myitoa(int num, char* str)
{
int dec = 1;;//确定数字的位数,初始值为1.
int i = 0;//用于跟踪字符串中的位置,初始值为0
int temp;//用于存储num的临时值
//判断用户输入的数据是否为负数
if (num < 0)
{
str[i++] = '-';
num = -num;
}
temp = num;//将num存储给temp
//判断这个数据的位次,是个位还是千位
while (temp > 9)
{
dec *= 10;
temp /= 10;
}
//将while (dec != 0),将整数num按照最高位到最低位逐个转换为字符,并存储到字符串str中
while (dec != 0)
{
str[i++] = num / dec + '0';//通过整数除法num / dec得到当前位的数字,然后加上字符'0'的ASCII值,将数字转换为对应的字符
num = num % dec;//通过取模运算num % dec去除已处理的最高位,以便处理下一位,说人话就是去除当前未位次,向下一位移动
dec /= 10; //将dec除以10,移动到下一位的权值
}
str[i] = '\0';
return 0;
}
void test7()
{
char str[10];
int a;
printf("请输入一个数:");
scanf("%d", &a);
int result = myitoa(a, str);
printf("返回的结果是:%s\n", str);
}
//主函数
int main()
{
/*
索引 作用
test1 小甲鱼作业6,打印结果为9分析
test2 类实现itoa将用户输入的整型转化为字符串形式存储(省内存)
test3 类itoa实现将'整型数据'转化为'字符型'(初级版本:强制转化版本)
test4 类实现itoa函数实现将'字符型'转化为'整型'(sprintf函数)
test5 类实现itoa函数实现将'字符数据'转为'整型'(snprintf函数)
test6 类实现itoa函数实现将'字符串型'转化为'整型'
test7 类实现itoa函数实现将'整型'转化为'字符型(小甲鱼课后作业0)
*/
test7();
return 0;
}
9.函数
9.1 定义
函数认识:
函数在编程中,函数(或称为方法、子程序)是一段特定的代码块,它执行一个单一的任务并可以被程序的其他部分重复调用。
函数的主要目的是将程序分解为更小、更可管理的部分,使代码更易于理解和维护。函数可以接受输入参数,并根据这些参数执行相应的操作。
此外,函数还可以返回一个值作为输出。
函数高阶理解: https://www.runoob.com/cprogramming/c-functions.html
9.2 为什么需要函数?
一句话核心理解
👉 函数的作用 = 把复杂问题拆成小问题
就像:
你不会把做饭的所有步骤写成一段几千字的流水账,而是分成
👉 洗菜 → 切菜 → 炒菜 → 装盘
程序也是一样。
9.2.1 可读性:让程序像“说明书”一样清晰
想象下面两种代码:
❌ 全写在 main 里
int main() {
// 读文件
// 解析数据
// 排序
// 计算平均值
// 输出结果
}
如果每一步都有 100 行代码:
👉 main 直接变成 500+ 行怪兽
你以后再看:
❓ 这段代码到底在干嘛?
✅ 用函数拆开
int main() {
readFile();
parseData();
sortData();
calculateAverage();
printResult();
}
现在 main 像一本目录:
👉 一眼就知道程序流程
这叫:
👉 结构化编程
9.2.2 复用性:同样的功能不用写两遍
假设你要多次计算最大值:
❌ 写在 main 里
// 第一处
int max = ...;
// 第二处
int max = ...; // 又写一遍
问题:
👉 改 bug 时你要改很多地方
👉 容易漏
✅ 用函数
int findMax(int arr[], int n);
想用几次就用几次:
findMax(a, n);
findMax(b, m);
这叫:
👉 代码复用
就像数学公式一样可以重复用。
9.2.3 易维护:修改时不影响全局
如果所有代码都在 main:
👉 改一点小功能
👉 可能影响整个程序
但用函数后:
主程序
├── 函数A
├── 函数B
└── 函数C
每个函数像一个独立模块
你可以:
✅ 单独测试
✅ 单独修改
✅ 不影响其他部分
这叫:
👉 模块化设计
9.2.3 易调试:找 bug 更简单
如果程序出错:
没有函数:
👉 你要在几千行代码里找 bug
像在垃圾堆里找针。
有函数:
你可以快速定位:
问题在 sortData()
只检查这个函数即可。
9.2.4 团队合作:多人开发必须用函数
现实项目不是一个人写:
小明:写网络模块
小红:写界面模块
小王:写算法模块
每个人负责不同函数。
如果所有代码都塞进 main:
👉 根本没法分工。
一个生活类比(非常重要)
想象你要造一辆汽车:
❌ 全堆在一起
把发动机、轮胎、座椅全部混在一个大铁块里。
👉 根本修不了。
✅ 模块化设计
发动机系统
刹车系统
电控系统
车身系统
每个系统对应:
👉 一个函数/模块
程序也是这样设计的。
如果真的全部写在 main 会发生什么?
会出现一个经典问题:
👉 意大利面条代码(Spaghetti Code)
特点:
- 逻辑混乱
- 难以阅读
- 难以修改
- 容易出 bug
- 几乎无法维护
很多新手写程序都会经历这个阶段 😂
最终总结(考试级记忆版)
程序需要函数是因为:
- 提高可读性 —— 代码更清晰
- 实现复用 —— 避免重复写代码
- 模块化设计 —— 方便维护
- 便于调试 —— 更容易找 bug
- 支持团队协作 —— 工程级开发必须
9.3 例子
程序1
include<iostream>
//定于函数
void print_F();//打印'F'
void print_I();//打印'I'
void print_S();//打印'S'
void print_H();//打印'H'
void print_C();//打印'C'
void print_F()
{
printf("########\n");
printf("## \n");
printf("## \n");
printf("###### \n");
printf("## \n");
printf("## \n");
printf("## \n");
}
void print_I()
{
printf("####\n");
printf(" ## \n");
printf(" ## \n");
printf(" ## \n");
printf(" ## \n");
printf(" ## \n");
printf("####\n");
}
void print_S()
{
printf(" ###### \n");
printf("## ##\n");
printf("## \n");
printf(" ###### \n");
printf(" ##\n");
printf("## ##\n");
printf(" ###### \n");
}
void print_H()
{
printf("## ##\n");
printf("## ##\n");
printf("## ##\n");
printf("########\n");
printf("## ##\n");
printf("## ##\n");
printf("## ##\n");
}
void print_C()
{
printf(" ###### \n");
printf("## ##\n");
printf("## \n");
printf("## \n");
printf("## \n");
printf("## ##\n");
printf(" ###### \n");
}
//程序1
//竖向打印字符串
void test1()
{
print_F();
printf("\n");
print_I();
printf("\n");
print_S();
printf("\n");
print_H();
printf("\n");
print_C();
printf("\n");
}
int main()
{
test1();
return 0;
}
程序2
//程序2
//实现横屏打印HACKED
//定于每一个字符占行为7
/*第一行的反斜杠'/'其实是一个续行符(是转移字符的一种),它的作用是告诉编译器下一行的文本是当前字符串的一部分,而不是一个新的字符串。
这在代码格式化时很有用,可以让字符串的每一行在源代码中单独显示,提高代码的可读性。
但是需要注意的是,这个反斜杠后面不能有任何字符,否则编译器会报错
例如:
const char* letters[] = {
"\abc" // 错误的用法,反斜杠后面不能有字符
}
转义字符:
转义字符是指在字符串中使用反斜杠(\)来表示一些特殊字符或控制字符。这些特殊字符在编程语言中具有特定的含义,而不是它们字面的含义。
通过使用转义字符,可以在字符串中包含这些特殊字符。
以下是一些常见的转义字符及其含义:
\n:换行符,将光标移动到下一行的开头。
\t:制表符,将光标移动到下一个制表位。
\\:反斜杠字符本身。
\':单引号字符。
\":双引号字符。
\0:空字符,表示字符串的结束。
*/
//这里定义了一个常量指针数组,将每一个字符图形视作一个字符串
const char* letters[] = {
"\
\## ##@\
\## ##@\
\## ##@\
\#############@\
\## ##@\
\## ##@\
\## ##@\
",
"\
\ # @\
\ # # # @\
\ ## ## @\
\ ######### @\
\ ## ## @\
\ ## ## @\
\## ##@\
",
"\
\ ############ @\
\## ##@\
\## @\
\## @\
\## @\
\## ##@\
\ ############ @\
",
"\
\## ##@\
\## ## @\
\## ## @\
\#### @\
\## ## @\
\## ## @\
\## ##@\
",
"\
\#############@\
\## @\
\## @\
\#############@\
\## @\
\## @\
\#############@\
",
"\
\########### @\
\## ##@\
\## ##@\
\## ##@\
\## ##@\
\## ##@\
\########### @\
"
};
void test2()
{
int gap;
printf("请输入字符间隔:");
scanf("%d", &gap);
int HEIGHT = 7;//每个字符串(也就是字符图形的高为7)
int i, j;
for (i = 0; i < HEIGHT; i++)
{
for (j = 0; j < 6; j++)
{
int k = 0;//用于迭代每行'#'字符
//计算现在字母每行有多少个字符
int len = strlen(letters[j]) / HEIGHT;//遍历每个字符串,计算出一个字符串的每行宽度
//计算当前打印第几行
int line = i * len;
//遇到 @ 时则标志改行结束;当遇到 # 打印
while (letters[j][line + k] != '@')
{
putchar(letters[j][line + k]);
k++;
}
//打印字符间的间隔(空格)
int temp = gap;
while (temp--)
{
putchar(' ');
}
}
putchar('\n');
}
}
//主函数区域;
int main()
{
test2();
return 0;
}
9.4函数的分文件编写(必学)
前面我们学了函数的使用,千万别把所有代码都写在主函数中口牙(/‵Д′)/~ ╧╧
现在我们学习函数的分文件编写,为什么需要分文件便携呢
如果你稍微写一个大一点的程序
里面包含:登陆模块(500行),文件处理(600行),加密算法(1000行)
一个文件就可以变成2000+行超级大文件,那咱找一个函数就像在垃圾桶翻东西,而且在写代码的时候还得想清楚函数的作用域,这个函数名有没有被声明过,这就很痛苦了。最重要的就是影响效率了
把程序拆分成多个逻辑模块,使得每一个模块都负责一件事情,最重要的是,可以重复使用呀。也就是另一个文件也可以调用,相当于工具的反复使用。
本次使用的环境为vs2022
函数内容:
//交换函数.cpp
#include<iostream>
#include"swap.h"//自己定义的需要添加双引号
int main()
{
int a, b;
cin >> a >> b;
swap(a, b);
return 0;
}
//swap.h
#pragma once
#include<iostream>
using namespace std;//如果不写的话,swap.cpp文件会因为找不到std::cout而无法使用
//交换函数
//参数列表:a,b
void swap(int a, int b);
//swap.cpp
#include"swap.h"//.cpp文件中包含声明的文件
void swap(int a, int b)
{
cout << "swap begin:" << " " << "a = " << a << ";b = " << b << endl;
int tmp;
tmp = a;
a = b;
b = tmp;
cout << "swap after:" << " a = " << a << ";b = " << b << endl;
}

新建文件:对指定的文件夹右键-添加-新建项目

这样子就是一个完整的程序编写
9.5 扩展
你是否有这样的疑惑捏?
为啥,主函数文件是一个main.cpp在里面包含的是include"swap.h",这个不是声明函数文件吗?编译器是怎么知道函数在swap.cpp中的
C++ 编译其实分 4 个阶段
以 GCC 或 Microsoft Visual Studio 为例,一个项目的构建流程是:
预处理 → 编译 → 汇编 → 链接
关键就在最后的 链接阶段。
容易出错的点
.h 文件根本不会被“单独编译”
当你写:
#include "swap.h"
发生的事情是
👉 预处理器直接把 swap.h 的内容“复制粘贴”进 main.cpp
就像这样:
// main.cpp
void swap(int&, int&); // 来自 swap.h
int main() {
...
}
所以:
👉 .h 的作用只是 告诉编译器函数的声明
它不包含真正的函数代码。那 swap.cpp 在干嘛?
#include"swap.h"//.cpp文件中包含声明的文件
void swap(int a, int b)
{
cout << "swap begin:" << " " << "a = " << a << ";b = " << b << endl;
int tmp;
tmp = a;
a = b;
b = tmp;
cout << "swap after:" << " a = " << a << ";b = " << b << endl;
}
编译时发生的是:
第一步:分别编译每个 .cpp
编译器会:
main.cpp → main.o
swap.cpp → swap.o
此时:
main.o 里:
- 有 main 函数
- 知道 swap 函数“存在”
- 但不知道它的实现
- 留下一个“未解析符号”
可以理解为:
我需要一个叫 swap 的函数
谁有?
swap.o 里:
我这里定义了 swap!
真正关键:链接阶段
链接器会把所有 .o 文件放在一起:
main.o + swap.o → 可执行文件
链接器会做一件事:
👉 匹配函数名字
main.o 需要 swap
swap.o 提供 swap
✔ 成功匹配 → 链接成功所以编译器“怎么知道”的真正答案
👉 它根本不需要知道 .h 和 .cpp 的对应关系
它只做两件事:
1️⃣ 编译阶段:检查声明是否正确
.h 只是让编译器知道:
这个函数存在
参数类型是这样
返回值是这样
如果你写错:
int swap(int a, int b); // 声明
但实现是:
void swap(int& a, int& b) { ... }
👉 编译或链接就会出错。
2️⃣ 链接阶段:按名字匹配实现
链接器只看:
有没有人真正定义这个函数?
如果没有:
undefined reference to swap
你肯定见过这个错误 😄
六、为什么习惯上 .h 和 .cpp 配套?
这是一种 工程约定,不是编译规则:
swap.h → 对外接口
swap.cpp → 具体实现
目的是:
👉 人类看得懂
👉 方便维护
👉 结构清晰
编译器其实不在乎文件名。
你甚至可以:
abc.cpp 实现 swap
xyz.cpp 调用 swap
只要链接时它们一起参与编译:
g++ main.cpp abc.cpp
就能成功。
一个非常直观的比喻
想象你在招聘:
main.cpp:
我需要一个叫 swap 的员工
swap.cpp:
我就是 swap
链接器:
好,你们匹配成功,上班!
.h 文件相当于:
👉 招聘启事(岗位说明书)
10.函数指针和指针函数
10.1指针函数:
1.含义
我们说函数的类型,事实上指的就是函数的返回值。根据需求,一个函数可以返回字符型、整型和浮点型这些类型的数据,当然,它还可以返回指针类型的数据。定义的时候只需要跟定义指针变量一样,在类型后边加一个星号即可
指针函数就是一个函数,在函数调用时,会返回一个指针(即某个变量的地址),可以通过返回的指针间接操作函数内部的内容
2.格式:
数据类型(int,void,float) *函数名(参数列表)
3.特点:
函数的返回值是一个地址(即指针)。
调用该函数后,可以通过返回的地址操作相应的数据。
10.1.1 详细解释
指针函数,本质上就是一个普通函数,唯一的特殊之处在于:这个函数执行完后,返回值不是普通的数值(比如 int、float),而是一个指针(内存地址)。
以用一个生活例子类比:
- 普通函数:你去餐厅点单,服务员(函数)直接把做好的菜(普通返回值,比如 int 型的数字 5)递给你。
- 指针函数:服务员不直接递菜,而是递给你一张桌号单(指针),你拿着这张单就能找到对应的餐桌(内存地址),餐桌上才是你要的菜(数据)。
10.1.2 指针函数的语法格式
返回值类型* 函数名(参数列表);比如:
int* get_num();:返回 int 类型指针的函数char* get_name();:返回 char 类型指针的函数
10.1.3实用代码例子(更简单的版本)
下面这个例子能直观看到指针函数的作用,我们定义一个指针函数,返回数组中第一个偶数的地址:
#include<stdio.h>
//遍历数组寻找第一个偶数
int* find_frist_even(int arr[],int len)
{
for(int i = 0;i < len ;i++)
{
if(arr[i]%2 == 0)
{
return &arr[i];
}
}
return NULL;
}
int main()
{
int numbers[] = {1,2,3,4,5,6,7};
int len = sizeof(numbers) / sizeof(number[0]);
int *even_ptr = find_first_even(numbers,length);
//指针只指向指针数组,指针数组访问返回的地址,
if(even_ptr != NULL){
printf("找到的第一个偶数是:%d\n",*even_ptr);
printf("找到的第一个偶数的内存地址是:%p\n",even_ptr);
}
else{
printf("数组中没有偶数!\n");
}
return 0;
}
为什么需要指针函数?(未来开发用的到的思想)
新手可能会问:直接返回数值不就行了?指针函数的核心价值在于:
- 当需要返回大块数据(比如大数组、结构体)时,返回指针比拷贝整个数据更节省内存、效率更高;
- 可以返回动态分配的内存(比如用
malloc申请的内存),让调用者后续操作这块内存。
10.2 函数指针
10.2.1 含义
函数指针,本质是一个指针变量(和普通指针一样占内存、存地址),但它不指向普通的数据(比如 int、数组),而是指向函数的入口地址。
通俗一点普通指针:
像你手里的 “家门钥匙”,指向的是 “家(数据)” 的位置;
函数指针:像你手里的 “遥控器”,指向的是 “电器(函数)” 的开关 —— 遥控器本身不干活,按下按钮就能触发对应的电器工作,函数指针本身不执行逻辑,调用它就能触发指向的函数执行。
#include <stdio.h>
// 步骤1:定义一个普通函数(比如加法)
int add(int a, int b) {
return a + b;
}
int main() {
// 步骤2:定义函数指针,格式:返回值类型 (*指针名)(参数类型)
// 这里:int是返回值,(*p)表示是函数指针,(int,int)是参数类型
int (*p)(int, int);
// 步骤3:让函数指针指向add函数(函数名就是函数的地址,直接赋值)
p = add;
// 步骤4:用函数指针调用函数(两种写法,效果一样)
// 写法1:直接用指针调用
int res1 = p(3, 5);
// 写法2:用*解引用后调用(更直观体现“指针指向函数”)
int res2 = (*p)(3, 5);
printf("p(3,5) = %d\n", res1); // 输出8
printf("(*p)(3,5) = %d\n", res2); // 输出8
printf("直接调用add(3,5) = %d\n", add(3,5)); // 输出8
return 0;
}
int add(int a, int b):就是一个普通的加法函数,没任何特殊;
int (*p)(int, int):定义函数指针p,你可以理解为:
- 给
p定一个 “规矩”:它只能指向 “返回 int、接收两个 int 参数” 的函数; - 括号
(*p)不能少,少了就变成 “指针函数”(之前讲过的),这是新手最容易错的点;
p = add:把add函数的 “入口地址” 赋值给p,就像把 “遥控器” 对准 “加法电器”;
p(3,5) 或 (*p)(3,5):用遥控器触发 “加法电器” 工作,和直接调用add(3,5)结果完全一样
p(3,5) = 8
(*p)(3,5) = 8
直接调用add(3,5) = 8
第二步:再看 “为什么要用函数指针”(核心是 “灵活切换”)
你可能会问:“直接调用 add 不就行了,为啥要多此一举用指针?”
答案是:函数指针能让你 “换函数不用改代码”,比如下面的例子,切换加法 / 减法,只需要改一行赋值,不用改调用逻辑。
#include <stdio.h>
// 定义两个普通函数:加法、减法(都符合“返回int、两个int参数”的规矩)
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int main() {
// 定义函数指针p(规矩不变)
int (*p)(int, int);
// 第一步:让p指向加法函数
p = add;
printf("3 + 5 = %d\n", p(3,5)); // 输出8
// 第二步:只改这一行,让p指向减法函数
p = sub;
printf("8 - 2 = %d\n", p(8,2)); // 输出6
// 核心:调用逻辑都是p(参数),只是p指向的函数变了
return 0;
}
函数指针就像一个 “通用开关”,只要函数符合 “返回值 + 参数” 的规矩,就能切换指向;
如果不用函数指针,你需要写:
printf("3+5=%d\n", add(3,5));
printf("8-2=%d\n", sub(8,2));
看起来区别不大,但如果是 10 个、100 个函数,函数指针的优势就出来了(比如下面的第三步)。
第三步:函数指针数组(解决 “多函数调用” 的麻烦)
如果有加减乘除 4 个函数,不用函数指针的话,你需要写 4 个printf+4 次函数调用;用函数指针数组,只需要一个循环就能搞定。
#include <stdio.h>
// 4个普通函数:加减乘除(规矩一致)
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divi(int a, int b) { return b!=0 ? a/b : 0; }
int main() {
// 定义函数指针数组:把4个函数的地址存到数组里
// 数组里的每个元素都是“int(*)(int,int)”类型的函数指针
int (*funcs[])(int, int) = {add, sub, mul, divi};
// 对应的运算符,方便打印
char ops[] = {'+', '-', '*', '/'};
// 数组长度:4个元素
int len = 4;
int a = 10, b = 2;
// 一个循环调用4个函数,不用写4次if/printf
for (int i=0; i<len; i++) {
// funcs[i]就是数组里的第i个函数指针,调用方式:funcs[i](a,b)
printf("%d %c %d = %d\n", a, ops[i], b, funcs[i](a,b));
}
return 0;
}
运行结果:
10 + 2 = 12
10 - 2 = 8
10 * 2 = 20
10 / 2 = 5
10.2.2 总结(新手必记 3 个要点)
- 函数指针的核心:是一个 “指向函数的指针变量”,定义格式
返回值 (*指针名)(参数类型),赋值直接写指针名=函数名,调用写指针名(参数); - 核心优势:不用改调用逻辑,只改赋值语句,就能切换调用不同的函数;
- 常用场景:函数指针数组可替代冗长的重复调用代码,回调函数(排序 / 遍历)可让函数更通用。
恭喜你已经了解了指针的篇章,这些都是了解即可,考试也不会考到,但是核心的思想是我们写代码所需要了解的东西
11.指针和引用
11.1 引用(指针变量)
引用变量是其他变量的别名。如同一个人的外号或小名, 共用同一块内存
定义引用变量时必须指明其引用的是哪个变量,且不能再指向其他变量。
比如你叫 “张三”,朋友都叫你 “小三”,“小三” 就是你的别名,叫 “张三” 或 “小三”,指的都是你这个人。
关键特点:
- 引用不占用独立内存空间(编译器层面处理,逻辑上是别名);
- 必须在定义时初始化,且一旦绑定某个变量,就不能再指向其他变量;
- 不能为空(必须绑定有效变量);
- 无需解引用,直接使用即可(和原变量用法完全一致)。
#include <iostream>
using namespace std;
int main() {
int a = 10; // 普通变量a,值为10
int& ref = a; // ref是a的引用(别名),必须初始化
cout << "a的值:" << a << endl; // 输出:10
cout << "ref的值:" << ref << endl; // 输出:10(和a共享值)
cout << "a的地址:" << &a << endl; // 输出:0x61fe14
cout << "ref的地址:" << &ref << endl; // 输出:0x61fe14(和a共享地址)
ref = 20; // 修改ref,等价于修改a
cout << "修改后a的值:" << a << endl; // 输出:20
return 0;
}
11.2 指针(指针变量)
指针是一个变量,它存储另一个变量的内存地址。
指针可以用 nullptr 或 0 初始化为空指针。
需要通过 * 解引用才能访问指向的变量。
指针是一个路标,标识了目的地的地址,好比如高速路口上的距离知识牌,指针可以指向别的地方(歪,你该不会忘记了吧,上面刚讲完 ̄へ ̄)
#include <iostream>
using namespace std;
int main() {
int a = 10; // 普通变量a,值为10,占用一块内存(假设地址是0x61fe14)
int* p = &a; // 指针变量p,存储a的地址(&a是取a的地址)
cout << "a的值:" << a << endl; // 输出:10
cout << "a的地址:" << &a << endl; // 输出:0x61fe14(实际地址随系统变化)
cout << "p的值(a的地址):" << p << endl; // 输出:0x61fe14
cout << "通过指针访问a的值:" << *p << endl; // *p是解引用,访问指针指向的变量,输出:10
return 0;
}
| 特性 | 指针 | 引用 |
|---|---|---|
| 本质 | 存储地址的变量 | 变量的别名 |
| 内存占用 | 占用独立内存(4/8 字节) | 不占用独立内存 |
| 初始化 | 可定义时不初始化(但不推荐) | 必须定义时初始化 |
| 指向变更 | 可随时指向其他变量的地址 | 一旦绑定,无法变更指向 |
| 空值 | 可以为 NULL/nullptr | 不能为空 |
| 解引用 | 需要用*解引用访问目标变量 |
无需解引用,直接使用 |
| 多级使用 | 支持多级指针(如int**) |
无多级引用(int&&是右值引用,非多级) |
| 函数参数 | 传指针需要检查是否为空 | 传引用无需检查(默认非空) |
12.结构体
12.1 基本概念
结构体(struct) 是一种自定义数据类型,它可以把多个不同类型的数据“打包”在一起,变成一个整体。
简单说:
👉 结构体 = 把多个变量组合成一个新的类型。
三种结构体创建方式:
struct 结构体名 变量名
struct 结构体名 变量名 = {成员1值,成员2值...}
定义结构体时顺便创建变量
通俗例子:
假设你要表示一个学生的信息:
- 姓名(字符串)
- 年龄(整数)
- 成绩(浮点数)
如果不用结构体,你可能会写:
string name;
int age;
float score;
但这样很乱 —— 这三个变量之间没有明确的“绑定关系”。
如果有 100 个学生怎么办? 你难道要写 300 个变量?
用结构体来解决
struct Student
{
string name;
int age;
float score;
};
这段代码的意思是:
我自己定义了一种新类型,叫 Student
这个类型里面有三个成员:name、age、score
怎么使用结构体?
1️⃣ 定义变量
Student s1;
这就相当于创建了一个“学生”。
2️⃣ 给成员赋值
s1.name = "张三";
s1.age = 18;
s1.score = 95.5;
3️⃣访问成员
用 . 访问
cout << s1.name << endl;
本质理解(底层一点)
结构体在内存中是:
| name | age | score |
是一块连续的内存空间。
所以:
-
结构体变量本质上是一个“组合数据块”
-
它让数据更有组织性
为什么要用结构体?
如果不用结构体:
string name[100];
int age[100];
float score[100];
你会发现:
- 很难管理
- 不直观
- 容易写错
用结构体后:
Student stu[100];
瞬间清晰很多。
12.2 注意
1️⃣ struct 是一种类型,不是变量
struct Student {}; // 只是定义类型
2️⃣ 结构体变量才会分配内存
Student s1; // 这里才真正开辟内存
3️⃣ 可以嵌套结构体
struct Date
{
int year;
int month;
int day;
};
struct Student
{
string name;
Date birthday;
};
4️⃣ 可以配合指针使用
Student* p = &s1;
cout << p->name;
八、一句话总结
结构体就是:
把多个相关变量打包成一个整体,让数据更有组织、更清晰。
12.3 示例
#include<iostream>
using namespace std;
struct Student {
//成员列表
//姓名
string named;
//年龄
int age;
//分数
int sorce;
}s3;
int main()
{
//第一创建方式
struct Student s1;
//s1赋值
s1.named = "张三";
s1.age = 18;
s1.sorce = 100;
cout << "姓名:" << s1.named << " 年龄:" << s1.age << " 分数:" << s1.sorce << endl;
//第二种创建方式 struct Student s2 = {..};
struct Student s2 = { "李四", 19, 80 };
//在定义结构体的同时创建结构体变量
s3.named = "王五";
s3.age = 20;
s3.sorce = 60;
cout << "姓名 :" << s3.named << " 年龄 :" << s3.age << " 分数 :" << s3.sorce << endl;
}
12.4 底层理解结构体
我们来画一个真正能帮你理解的内存图 👇
假设我们有这样一个结构体:
struct Student
{
char name[8]; // 8字节
int age; // 4字节
float score; // 4字节
};
然后定义:
Student s1;
一、内存是怎么存的?
内存是“一格一格字节”排开的。
假设 s1 从地址 0x1000 开始存储。
📦 内存布局图
地址 内容
---------------------------------
0x1000 name[0]
0x1001 name[1]
0x1002 name[2]
0x1003 name[3]
0x1004 name[4]
0x1005 name[5]
0x1006 name[6]
0x1007 name[7]
0x1008 age (第1字节)
0x1009 age (第2字节)
0x100A age (第3字节)
0x100B age (第4字节)
0x100C score (第1字节)
0x100D score (第2字节)
0x100E score (第3字节)
0x100F score (第4字节)
二、结构体在内存里的本质
结构体就是:
| name | age | score |
它在内存里是:
✅ 连续存储
✅ 一整块空间
✅ 总大小 = 所有成员大小之和(可能会有对齐)
三、结构体大小是多少?
sizeof(Student)
这里是:
8 (name)
+ 4 (int)
+ 4 (float)
= 16 字节
所以:
s1 占 16 个字节
四、如果赋值后内存长什么样?
假设:
strcpy(s1.name, "Tom");
s1.age = 18;
s1.score = 95.5;
内存会变成:
| T | o | m | \0 | ? | ? | ? | ? | age(4字节) | score(4字节) |
- 字符按 ASCII 存
- int 按二进制存
- float 按 IEEE754 存
五、结构体地址关系
&s1 → 0x1000
&s1.name → 0x1000
&s1.age → 0x1008
&s1.score → 0x100C
记住一句话:
结构体的地址 = 第一个成员的地址
六、升级一点:什么是内存对齐?
如果我们改成:
struct Test
{
char a; // 1字节
int b; // 4字节
};
理论上是:
1 + 4 = 5字节
但实际:
sizeof(Test) // 通常是 8
为什么?
因为:
| a | 填充 | 填充 | 填充 | b(4字节) |
👉 编译器会自动补空字节,让 int 按 4 字节对齐
这叫:内存对齐
七、用一句话理解结构体的内存
结构体就是:
把多个变量按顺序排列在一块连续内存里。
13.结构体数组
13.1 基本概念
结构体数组是由多个相同类型的结构体元素组成的数组。每个结构体元素包含一组相关的数据字段(可以是不同类型的)。结构体数组允许你一次性处理多个结构体对象,而无需单独定义每一个结构体变量。
假设你有一个班级,每个学生都有姓名、年龄和成绩,结构体数组就像是你为班级每个学生创建的“学生档案”。
想象一下:
你是班主任,需要管理 3 个学生的信息,每个学生的信息都包括 姓名、年龄 和 成绩。为了方便管理,你决定把所有学生的信息按顺序整理成一个“学生档案袋”,每个学生有自己的资料卡,整个档案袋就是结构体数组。
比喻:
- 结构体:就像每个学生的资料卡,包含了姓名、年龄和成绩。
- 结构体数组:就像整个学生档案袋,里面放着每个学生的资料卡(即结构体)。
13.2 例子
#include<iostream>
#include<string>
using namespace std;
struct Student{
char name[50];
int age;
string sex; // 使用 string 类型来存储性别,char不能存储中文字节,因为中文字符一般占据2-4字节,char只能存储1个字节的字符
float score;
};
int main()
{
// 创建结构体数组
Student s[3] = {
{"Limhope", 18, '女', 90.5}, // 使用字符串 "女"
{"HACKED", 20, '男', 60.0}, // 使用字符串 "男"
{"Teay Bear", 21, '男', 99.0}
};
//给结构体数组中的元素赋值
s[2].name = "flag";
s[2].age = 25;
s[2].name = "女";
s[2].score = 30;
//打印结构体
for(int i = 0; i < 3; i++)
{
cout << "学生的名字:" << s[i].name
<< ", 学生的年龄:" << s[i].age
<< ", 学生的性别: " << s[i].sex
<< ", 学生的成绩: " << s[i].score << endl;
}
return 0;
}
14.结构体指针
14.1 基本概念
结构体指针其实就是 指向结构体变量的指针。它的本质和普通指针一样,只不过指针指向的是一个结构体类型的数据。通过结构体指针,我们可以更灵活地访问和操作结构体成员。
14.2 作用
1.访问结构体成员 使用
->运算符可以通过指针直接访问结构体的成员。 这在需要频繁传递结构体数据时非常方便。
2.节省内存和提高效率 如果结构体很大,直接传递结构体变量会复制整个数据,效率低。 使用指针只传递地址,不需要复制整个结构体。
3.动态内存分配 可以用new或malloc在堆上创建结构体,然后用指针管理它。 这样结构体的生命周期可以由程序员控制。
4.函数参数传递 在函数中传递结构体指针,可以修改原始结构体的内容,而不是副本。
14.3 例子
#include <iostream>
#include <string>
using namespace std;
struct Student{
string name;
string sex;
int age;
double score; // 用 double 更精确
};
int main() {
Student s1 = { "张三", "男", 20, 90.5 };
Student* p = &s1;
//通过指针访问结构体变量->访问
cout << "姓名:" << p->name
<< "\n性别:" << p->sex
<< "\n年龄:" << p->age
<< "\n分数:" << p->score << endl;
//直接访问结构体变量
cout << "姓名:" << s1.name
<< " 性别:" << s1.sex << endl;
return 0;
}
15.结构体嵌套结构体
15.1 基本概念
在 C 语言中,结构体嵌套结构体是指在一个结构体中定义另一个结构体作为成员。换句话说,一个结构体可以包含另一个结构体,从而形成更复杂的数据类型。
15.2 作用与意义
嵌套结构体的主要作用是 分层管理和组织数据,让复杂的数据结构更清晰、更易维护。
作用总结:
- 数据分层管理:将相关信息分组,避免数据混乱。例如学生信息和成绩分开管理。
- 提高代码可读性:逻辑更清晰,结构更紧凑。
- 复用性强:内部结构体可以在多个外部结构体中使用,减少重复定义。
- 适合复杂场景:广泛应用于操作系统内核、嵌入式系统、网络协议解析、图形界面设计等
15.3 例子
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
string sex;
int age;
double score; // 用 double 更精确
};
struct Teacher {
string name;
string sex;
int age;
Student stu; // 结构体嵌套,负责的学生,和学生的关系
};
int main() {
//初始化老师
Teacher t1;
t1.age = 30;
t1.name = "HACKED";
//初始化学生
t1.stu.age = 18;
t1.stu.sex = "女";
t1.stu.name = "limhope";
t1.stu.score = 100.0;
std::cout << t1.name;
//打印老师信息
cout << "老师姓名:" << t1.name << " 老师性别:" << t1.sex
<< " 老师的年龄: " << t1.age << endl;
//打印学生信息
cout << "学生姓名:" << t1.stu.name
<< " 学生性别:" << t1.stu.sex
<< " 学生的成绩: " << t1.stu.score << endl;
}
16. 结构体做函数参数
16.1 作用
在 C 语言或 C++ 里,结构体作为函数参数的作用,主要是为了在函数之间传递一组相关的数据。它背后的概念可以分几个层次来理解:
16.2 为什么要用结构体做参数
- 打包数据:结构体可以把多个不同类型的变量(比如 int、float、char 数组)组合在一起,形成一个整体。函数只需要接收一个结构体参数,就能同时获得这些数据,而不用传很多个单独的变量。
- 提高可读性:相比传递一长串参数,传一个结构体更直观,代码更容易理解和维护。
- 逻辑清晰:结构体本身就是对数据的一种抽象,传递结构体参数能让函数的接口更符合实际问题的逻辑。
16.3 参数传递的方式
在函数中使用结构体参数时,有两种常见方式:
| 方式 | 特点 | 示例 |
|---|---|---|
| 值传递 | 函数接收的是结构体的副本,函数内部修改不会影响外部 | void func(Point p) |
| 指针传递 | 函数接收的是结构体的地址,函数内部修改会影响外部 | void func(Point *p) |
16.4 实例代码
#include<iostream>
#include<string>
using namespace std;
void printStruct2(struct Student s);
void printStruct1(struct Student* s);
struct Student
{
string name;
string sex;
int score;
};
//直接对形参进行修改,但是不会修改主函数的内容
void printStruct2(Student s)
{
s.name = "Teay Bear";
s.sex = "null";
s.score = 0;
cout << "学生的姓名: " << s.name << " 学生的性别: " << s.sex << " 学生的成绩: " << s.score << endl;
}
//通过指针进行修改,指针修改的话需要通过->
void printStruct1(Student* s)
{
s->name = "limhope";
s->score = 90.0;
s->sex = "女";
cout << "学生的姓名: " << s->name << " 学生的性别: " << s->sex << " 学生的成绩: " << s->score << endl;
}
int main()
{
Student s1 = { "HACKED","男",90.0 };
printStruct1(&s1);
printStruct2(s1);
return 0;
}
为什么指针能“省内存 / 提高性能”
•避免复制:把大的 struct 作为值传递时要完整拷贝一次(占用栈/CPU)。传指针/引用只传 4/8 字节地址,节省内存并更快。
(在 64 位系统上指针通常是 8 字节)
• 减少栈使用:拷贝大型结构体会增加栈帧大小;指针只占固定小空间。
• 修改原对象:用指针或引用可以在函数内修改调用者的对象(共享同一实例),不用返回修改后的副本。
• 支持动态/可空/所有权语义:指针可以指向堆上对象或为 nullptr,且配合智能指针管理生命周期。
17.结构体中的const使用
结构体加上
const,本质上是为了 限制对结构体变量或结构体成员的修改。但它的作用会因为使用方式不同而变化。
我们都知道结构体都可以是使用指针进行地址传递给函数进行调用,形如
void printScore(struct Student* s)
//Student是一个自定义的结构体
17.1 const struct A变量:整个结构体只读
const struct Point {
int x;
int y;
} p = {1, 2};
效果:
p.x、p.y都不能修改- 整个结构体是只读的
p.x = 10; // ❌ 编译错误
适用场景:
- 定义常量数据
- 防止误修改(比如配置、表项、查找表等)
17.2 const struct A指针:指针指向的结构体不可修改
struct Point p = {1, 2};
const struct Point *ptr = &p;
效果:
ptr指向的结构体内容不能改//指针无法篡改指向结构体的内容,只读权限只针对指针,如果是别的函数进行调用的话可以修改- 但
ptr本身可以指向别的结构体
ptr->x = 10; // ❌ 不允许
ptr = &other; // ✔️ 可以
适用场景:
- 函数参数只读,防止误修改
- 提高代码安全性
17.3 struct A const *ptr:指针本身不可改,但结构体可改
struct Point p = {1, 2};
struct Point * const ptr = &p;
效果:
ptr不能指向别的结构体- 但结构体内容可以修改
ptr = &other; // ❌ 不允许
ptr->x = 10; // ✔️ 可以
适用场景:
- 固定指针(比如驱动、底层代码)
17.4 const struct A const *ptr
const struct Point * const ptr = &p;
效果:
- 指针不能改
- 内容不能改
这是最严格的保护(最高保护,无法修改,只读权限)。
| 声明方式 | 指针可改 | 内容可改 | 用途 |
|---|---|---|---|
const struct A var |
❌ | ❌ | 整个结构体只读 |
const struct A *p |
✔️ | ❌ | 函数参数只读 |
struct A * const p |
❌ | ✔️ | 固定指针 |
const struct A * const p |
❌ | ❌ | 完全只读 |
18. 结构体的练习
案例描述:
学校正在做毕设项目,每名老师带领5个学生,总共有3名老师,需求如下
设计学生和老师的结构体,其中在老师的结构体中,有老师姓名和一个存放5名学生的数组作为成员
学生的成员有姓名、考试分数,创建数组存放3名老师,通过函数给每个老师及所带的学生赋值
最终打印出老师数据以及老师所带的学生数据。
#include<iostream>
#include<string>
#include<ctime>
#include<cstdlib>
using namespace std;
struct Student {
string name;
int score;
};
struct Teacher {
//老师的姓名
string name;
struct Student Sarray[5];
};
void printInf(Teacher Tarray[], int len);//打印结构体
void allocateSpace(Teacher Tarray[], int len);//写入信息
void allocateSpace(Teacher Tarray[], int len)
{
//给老师复制
string nameSeed = "ABCDE";
for (int i = 0; i < 3; i++)
{
Tarray[i].name = "Teacher_";
Tarray[i].name += nameSeed[i];//追加名字,Teachr_和nameSeed中的ABCDE进行拼接
for (int j = 0; j < 5; j++)
{
Tarray[i].Sarray[j].name = "Student_";
Tarray[i].Sarray[j].name += nameSeed[j];//追加名字,Student_和nameSeed中的ABCDE进行拼接
//srand(time(0));
int randomNumber = rand() % 101;
Tarray[i].Sarray[j].score = randomNumber;//给学生的成绩赋值
}
}
}
void printInfo(Teacher Tarray[], int len)
{
for(int i=0;i<len;i++)
{
cout << "老师的信息:" << Tarray[i].name << endl;
for(int j = 0;j<5;j++){
cout<<"\t老师学生的信息:"<<Tarray[i].Sarray[j].name <<" "<< Tarray[i].Sarray[j].score << endl;
}
}
}
int main()
{
//创建3名老师的数组
Teacher Tarray[3];
//通过函数给3名老师的信息复制,并且给老师带的学生复制
int len = sizeof(Tarray) / sizeof(Tarray[0]);
allocateSpace(Tarray, len);
//打印所有老师的information
printInfo(Tarray, len);
}
19.类和对象
19.1 概念
类用于指定对象的形式,是一种用户自定义的数据类型,它是一种封装了数据和函数的组合。类中的数据称为成员变量,函数称为成员函数。类可以被看作是一种模板,可以用来创建具有相同属性和行为的多个对象。
又回到了我们第一章前提的内容,还记得咩?
在编程世界里,类就像是一张建筑图纸。
- 类(Class):是图纸。它规定了房子应该有几扇窗户、门朝哪开、地暖怎么铺。但图纸本身不能住人,它只是一个定义和规范。
- 对象(Object / Instance):是根据图纸盖出来的真实的房子。你可以用同一张图纸在不同的小区盖出 100 栋房子,每一栋房子都是这个“类”的一个“实例”。
定义类的规范
类的声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方法描述共有接口
定义一个类需要使用关键字 class,然后指定类的名称,并类的主体是包含在一对花括号中,主体包含类的成员变量和成员函数。
定义一个类,本质上是定义一个数据类型的蓝图,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。

浙公网安备 33010602011771号