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)

image-20260320114025011


更新日志

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++头文件和std命名空间(精辟) - C语言中文网

还记得我们学习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等前缀,并直接使用等形式。这种命名方式主要是为了区别于C语言的标准库头文件,并且是为了避免命名冲突。


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_intadd_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++类和对象到底是什么意思(重要!)

C++类和对象到底是什么意思? - 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 就是一个 ,它描述了汽车有 brandyear,并且可以 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;
}

✅ 这里:

  • car1car2对象(具体的汽车)。
  • 它们的数据可以不同,但行为(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++有三种基本循环:
    1. for 循环(已知循环次数)
    2. while 循环(先判断条件)
    3. 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;   // 正确

👉 这里 pint 类型的指针,只能存放 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就可以用浏览器渲染啦

image-20260320113235364



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 个元素,再存第二行,以此类推);
  • 核心属性:
    1. arr 是二维数组名,代表整个二维数组的首地址;
    2. arr[i] 表示第 i 行的「一维数组名」,代表第 i 行的首地址;
    3. arr[i][j] 表示第 i 行第 j 列的具体元素。

image-20260202212146997
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 概念

指向指针的指针,顾名思义,就是一个指针,它存储了另一个指针的地址。也就是说,指向指针的指针是指向一个指针变量的指针。

解释:

  1. 指针是一个存储地址的变量,它指向某个数据的内存位置。
  2. 指向指针的指针,即二级指针,是指存储另一个指针地址的指针。

为什么需要指向指针的指针?

指向指针的指针在多级链表、动态内存管理(如双重指针传递动态数组的地址)等高级编程中很有用。


6.2 例子

程序1

void test1()
{
	int num = 520;
	int* p1 = &num;
	int** p2 = &p1;
 /*或许你这个有疑问,我们前面学过指针它本质上是一个地址是吧,那为什么还要取出p的指针的地址呢?你指向指针的指针不就是要一个地址吗?那 p 不就是一个地址吗?这里来个演示

	int num = 520;
	int* p = &num;
	printf("*p = %p\n", p);//这里的打印的是指针 p 的"值"(注意是值,不是本身),即 num 的地址。
	printf("*p = %p\n", &p);//这里才是打印指针 p 本身的地址。
	printf("num = %p", &num);//这里打印的就是 num 的地址,等效于注释中第三行代码。
	
	那这里你估计就有眉目了,
	int* p1 = &num;//这里定义了一个指针 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 = &num;//现在这个指针(不可修改)指向的是一个'变量'
	printf("num = %d,&num = %p\n", num, &num);
	printf("*pc = %d,pc = %p", *pc, pc);

	再例如:
	const int num = 520;
	const int* p = &num; // 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 = &num;   // 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 = &num;
		const  int** q = &p;
		printf("%d\n", **q);

7.4 指向'指向常量指针'的常指针

特点

指针指向不可修改,指针指向的值也不可以修改<专门专用,最高级保护>

const int num = 520;
const int * const p = &num;
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
  • 几乎无法维护

很多新手写程序都会经历这个阶段 😂


最终总结(考试级记忆版)

程序需要函数是因为:

  1. 提高可读性 —— 代码更清晰
  2. 实现复用 —— 避免重复写代码
  3. 模块化设计 —— 方便维护
  4. 便于调试 —— 更容易找 bug
  5. 支持团队协作 —— 工程级开发必须

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;


}


image-20260219224647898

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

image-20260219225032719

这样子就是一个完整的程序编写

9.5 扩展

你是否有这样的疑惑捏?

为啥,主函数文件是一个main.cpp在里面包含的是include"swap.h",这个不是声明函数文件吗?编译器是怎么知道函数在swap.cpp中的

C++ 编译其实分 4 个阶段

GCCMicrosoft 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 个要点)

  1. 函数指针的核心:是一个 “指向函数的指针变量”,定义格式返回值 (*指针名)(参数类型),赋值直接写指针名=函数名,调用写指针名(参数)
  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.动态内存分配 可以用 newmalloc 在堆上创建结构体,然后用指针管理它。 这样结构体的生命周期可以由程序员控制。
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.xp.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,然后指定类的名称,并类的主体是包含在一对花括号中,主体包含类的成员变量和成员函数。
定义一个类,本质上是定义一个数据类型的蓝图,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。

posted @ 2026-03-20 14:24  ssot_HACKED  阅读(64)  评论(1)    收藏  举报