1. 结构体
1.1. 为什么有结构体
数组只能存储相同类型数据项的变量,实际生活中一类物体的各个数据参数类型大概率不相同。结构体使我们描述物体更加全面准确。
1.2. 什么是结构体
结构体是一种用户自定义的可用的数据类型,它允许用户存储不同类型的数据项。
举个例子,我们定义一个学生数据类型,这个类型包含该学生的年龄、身高、体重、名字。用C语言表示一下:
struct Student { //Student是我们定义的数据类型,观察得到这个数据类型里面有三个类型
int age;
double height;
double weight;
char name[30];
};
上文的$Student$就是一个结构体,更为具体的描述我们留到$1.3.$结构的定义和访问中阐述
1.3. 结构体的定义、初始化与访问
1.3.1. 结构体的定义
1.3.1.1. 第一种定义方式
一般来说,用以下的形式定义的话结构体标签
和结构体变量
在定义时至少要出现一个
struct Student { //Student是 结构体标签(可以不写)
int age; //里面的变量是 结构体成员。结构体可以没有成员,但是这样做没有意义
double height;
double weight;
char name[30];
}Tom, Sam; //右括号和分号之间的是 结构体变量(可以不写)
上面的这个代码段声明了拥有4个成员的结构体,分别为整型的$age$,双精度的$height$,双精度的$weight$和长度为30的字符型数组$name$;该结构体的标签被命名为$Student$。同时声明了结构体变量$Tom$和$Sam$。
1.3.1.2. 第二种定义方式
还有一种结构体的定义方式
typedef struct Student{ /*Student是被创建的新类型,在这段代码后面可以将Student作为类型声明新的结构体变量*/
int age; //变量是结构体成员
double height;
double weight;
char name[30];
}Student; //这里的Student是可以不写的
1.3.1.3. 结构体的成员类型
在结构体的定义时其成员可以包含$int$、$double$等常规类型,也可以包含其他结构体,也可以包含指向自己的结构体类型的指针(数据结构),也可以包含函数。
1.3.1.4. 结构体的类型
结构体一经定义就是一个新的类型。需要注意的是一个程序里面的两个结构体即使成员完全一样,它们也是两个不一样的类型。
1.3.2. 结构体的初始化
1.3.2.1. 对结构体变量的初始化
结构体变量可以在定义时对其初始化:
struct Student {
int age;
double height;
double weight;
char name[30];
}Tom{20, 120, 170, "Tom Smith"}, Sam;
这个例子中在定义$Student$的结构体变量$Tom$时将其结构体成员完成初始化
想一想如果你需要写一个结构体来存储今年刚入学的学生信息,他们的年龄大多都是$19$岁,这时可以对上面的代码块进行修改:
struct Student {
double height;
double weight;
char name[30];
int age = 19; /*将初始化的值放在最后边,后面对该结构体变量初始化时可以只写前面没有初始化的变量而不产生歧义*/
}Tom{120, 170, "Tom Smith"}, Sam;
1.3.2.2. 对结构体函数的初始化
可以将函数初始化放在结构体里面;也可以在结构体体里面只给出函数的返回类型和函数名。如果我们现在要编写一个函数计算一个学生的年龄除以名字长度,有两种方法实现:
struct Student {
int age;
double height;
double weight;
char name[30];
double fun() { //在结构体内定义函数
return (double)age * 1.0 / strlen(name); //这里强制类型转化避免溢出
};
};
struct Student {
int age;
double height;
double weight;
char name[30];
double fun();
};
double Student::fun() { //在结构体外面定义函数
return (double)age * 1.0 / strlen(name);
}
1.3.3. 结构体的访问
1.3.3.1 对结构体成员的访问
访问结构体的成员用到的符号是(.)。下面用例子解释一下吧:
#include<stdio.h>
#include<string.h>
struct Student {
int age;
double height;
double weight;
char name[30];
double fun();
}Tom; //Tom是我们定义的Student结构体变量
double Student::fun() { //在结构体外面定义函数
return (double)age * 1.0 / strlen(name);
}
int main() {
Tom.age = 20; //使用(.)对结构体成员变量访问
strcpy(Tom.name, "Tom Smith"); //vs2019用strcpy_s
printf("%d %s\n", Tom.age, Tom.name);
printf("%.3f", Tom.fun());
return 0;
}
1.3.3.2. 将结构体作为参数,整体访问
#include<stdio.h>
#include<string.h>
struct Student {
int age;
double height;
double weight;
char name[30];
double fun();
}Tom;
double Student::fun() {
return (double)age * 1.0 / strlen(name);
}
void printStudent(Student s) { //将结构体作为参数整体访问
printf("%d %.3f %.3f %s\n", s.age, s.height, s.weight, s.name);
}
int main() {
Tom.age = 20;
Tom.height = 170.0;
Tom.weight = 150.0;
strcpy(Tom.name, "Tom Smith"); //vs用strcpy_s
printStudent(Tom);
return 0;
}
1.4. 结构体的简单拓展
1.4.1. 结构体数组
当结构体的成员变量里有指向该结构体类型的指针时,可以将其抽象为数据结构来使用。这里举个链表的例子帮助大家更好地理解。
还是在上文的代码基础上做拓展。假设现在有一些同学在食堂排成一列打饭,但就是有人不讲规矩要插队,有什么办法在编程里面将某一个人的前后人是谁表示出来吗?
最好像到的是结构体数组,这里用代码演示一下:
#include<stdio.h>
#include<string.h>
struct Student {
char name[30]; //vc6.0不支持新语法,vs2019写成 char name[30] = "";来初始化内存
};
int main() {
int i;
Student array[10];
strcpy(array[0].name, "A"); //vs用strcpy_s
strcpy(array[1].name, "B");
strcpy(array[2].name, "C"); //一开始ABC在按序排队
for(i = 0; i < 3; ++i)
printf("%s ", array[i].name);
//vs可以用for(auto& it : array) printf("%s ", array[i].name);简化代码量
printf("\n");
//接下来表示D插队到A的前面
char temp[30];
strcpy(temp, array[0].name);
strcpy(array[0].name, "D");
for (i = 3; i > 1; --i) {
strcpy(array[i].name, array[i - 1].name);
}
strcpy(array[1].name, temp);
for(i = 0; i < 4; ++i)
printf("%s ", array[i].name);
printf("\n");
return 0;
}
上面是用数组表示排队情况,仔细想想用数组表示有些麻烦:($1$)食堂各个时间的打饭人数不同,想要在每一时刻都能把排队情况表示出来需要创建一个能容纳最高峰时刻人数的数组,而对于食堂而言一天中的大部分时刻是没有这么多人的,对于空间而言是一种浪费;($2$)试想一下前面有人打完饭走了或有人插队时(插队不好,我们不要插队)数组里的元素要进行循环移动,对于时间而言也是一种浪费。
1.4.2. 用结构体实现单链表
于是我在这里引入单链表的概念来对这个实际生活中的情况在用数组表示的基础上加以优化:(只是简单地介绍下)下面的程序简单地模拟了下排队时有人插队以及第一个人打饭走后队伍的排队情况
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
typedef struct Student {
char name[30];
Student* next;
}LNode, * LinkList;
LinkList CreateList(int n);
void print(LinkList h); //输出排队的人
void insert(LinkList list, int n); //插队
void delet(LinkList list, int n); //第一个人打完饭走了(这种情况下n为1)
int main()
{
LinkList Head = NULL;
int n;
printf("一开始有几个学生? ");
scanf("%d", &n); //vs用2019
Head = CreateList(n);
printf("这些学生的名字是:\n");
print(Head);
printf("\n\n");
printf("输入插入的位置: ");
scanf("%d", &n);
insert(Head, n);
printf("在插队后队伍变成了: \n");
print(Head);
printf("\n");
delet(Head, 1);
printf("第一个人打完饭后队伍变成了: \n");
print(Head);
printf("\n");
return 0;
}
LinkList CreateList(int n)
{
char ch = getchar(); //吸收空格
LinkList L, p, q;
int i;
L = (LNode*)malloc(sizeof(LNode));
if (!L)return 0;
L->next = NULL;
q = L;
for (i = 1; i <= n; i++)
{
p = (LinkList)malloc(sizeof(LNode));
if (p != NULL) {
printf("请输入第%d个学生的名字:", i);
fgets(p->name, 30, stdin);
p->next = NULL;
q->next = p;
q = p;
}
}
return L;
}
void print(LinkList h)
{
LinkList p = h->next;
while (p != NULL) {
printf("%s", p->name);
p = p->next;
}
}
void insert(LinkList list, int n) {
char ch = getchar();
LinkList t = list, in;
int i = 0;
while (i < n && t != NULL) {
t = t->next;
i++;
}
if (t != NULL) {
in = (LinkList)malloc(sizeof(LNode));
puts("输入要插入的学生的名字: ");
if (in) {
fgets(in->name, 30, stdin);
in->next = t->next;
t->next = in;
}
}
else {
puts("节点不存在");
}
}
void delet(LinkList list, int n) {
LinkList t = list, in;
in = (LinkList)malloc(sizeof(LNode));
int i = 0;
while (i < n && t != NULL) {
in = t;
t = t->next;
i++;
}
if (t != NULL) {
if (in)
in->next = t->next;
free(t);
}
else {
puts("节点不存在");
}
}
2. 函数
2.1. 为什么有函数
- 减少重复的代码量。
- 使程序结构清晰、易读,易于修改,符合结构化程序设计原理 。
- 程序出错时方便分块查找出错误,便于调试
- 有些问题是递归思想,需要用函数实现递归算法
2.2. 什么是函数
- 函数将程序里面的某一功能模板化、将目的具体化,也方便了后期的修改和维护。
- 函数有较强的独立性,可以相互调用。
- 如果在一个程序中不同的地方,需要经常执行一组相同的语句来完成一种相对独立的功能,则可以把这组语句定义为一个独立的程序模块---函数。简化了代码量。
2.3. 函数的定义和调用
2.3.1. 一般定义
2.3.1.1. 格式
ReturnType FunctionName(ParameterList){
FunctionBody
}
在上面的定义中,我们可以看出一个函数的定义分四部分:
- $ReturnType$是返回类型,只要$ReturnType$不为$void$,函数的返回值类型必须要求和$ReturnType$相同
- $FunctionName$是函数的名字
- $ParameterList$是函数的参数,参数数量理论上为任意自然数,但太多了的话不利于后期的修改与维护
- $FunctionBody$是函数主体,包含一组函数执行语句
2.3.1.2. 局部变量
在函数内部或由花括号扩住的复合语句内部定义的变量就是局部变量。局部变量的作用域就是定义它的函数或复合语句块,所以称它们具有局部作用域。根据生存期的不同,局部变量又分为自动局部变量和静态局部变量。可以定义和全局变量同名的局部变量,则在该局部,局部变量屏蔽了同名的全局变量
2.3.1.2.1. 自动局部变量
在函数内部或由花括号扩住的复合语句内部定义的没有$static$的变量。会随着函数的结束而被销毁。
举个例子:
#include<stdio.h>
void fun() {
int i = 0; //自动局部变量
printf("%d ", i++);
}
int main() {
for(int i = 0; i < 5; ++i)
fun();
return 0;
}
2.3.1.2.2. 静态局部变量
在函数中使用$static$关键字定义的局部变量。和局部变量一样,具有局部作用域。和全局变量一样,具有静态生存期。一经定义就一直存在,即使函数执行结束,静态变量依然存在;函数下次被调用不会再创建新的静态变量。
举个例子:
#include<stdio.h>
void fun() {
static int i = 0; //静态局部变量
printf("%d ", i++);
}
int main() {
for(int i = 0; i < 5; ++i)
fun();
return 0;
}
2.3.1.2.3. 全局变量
全局变量的作用域是定义该变量的整个程序文件。全局变量的生存期就是程序的整个运行期。
举个例子:
#include<stdio.h>
int i = 0; //全局变量
void fun() {
printf("%d ", i++);
}
int main() {
for(int i = 0; i < 5; ++i)
fun();
return 0;
}
2.3.2. 内联函数
2.3.2.1. 为什么有内联函数
这要从函数调用的实现说起:函数通过栈调用。一个函数被调用时,系统首先在“栈”顶为其开辟一块空间,用来存放函数的形式参数和函数中定义的非静态局部变量(自动局部变量),以及函数调用时的现场信息(系统寄存器的值)和返回地址。函数运行结束时,系统根据“栈”中保存的现场信息和返回地址恢复主调函数的执行现场,返回到主调函数中继续执行;分配给被调函数的“栈”空间被自动释放。也就是说实现函数调用需要从时间和存储空间两方面付出代价。如果一个函数非常短而且在程序中需要频繁调用,那么对空间的节省并不明显,而多次函数调用所付出的代价却十分显著。于是诞生了内联函数。
2.3.2.2. 内联函数是什么
编译器会把内联函数的函数体直接嵌入到发生函数调用的地方,以取代函数调用语句,而不进行常规的函数调用。
2.3.2.3. 内联函数怎么用
语法如下:
inline 返回值类型 函数名(形参列表){
函数体;
}
2.3.3. 函数重载
2.3.3.1. 为什么有函数重载
简化代码量
2.3.3.2. 重载函数是什么
$C$语言允许在相同的作用域中定义函数名相同但参数形式不同的多个函数,参数形式不同是指:要么参数的类型不同,要么参数的个数不同。这样的编程技术称为函数重载,这组同名的函数称为重载的函数。
2.3.3.3. 重载函数怎么用
调用重载函数时,编译器根据实参的类型和实参的个数找到匹配的重载函数进行调用。下面举个例子帮助大家更好地理解:
#include<stdio.h>
int max(int num1, int num2)
{
return (num1 >= num2) ? num1 : num2;
}
double max(double num1, double num2) //调用的参数类型和第一个不一样
{
return (num1 >= num2) ? num1 : num2;
}
int max(int num1, int num2, int num3) //参数个数和其他的不一样
{
return (max(num1, num2) >= num3) ? max(num1, num2) : num3;
}
int main() {
int x1, x3;
double x2;
x1 = max(1, 2);
x2 = max(1.2, 2.2);
x3 = max(1, 2, 3);
printf("%d %.3f %d\n", x1, x2, x3);
return 0;
}
2.3.4.函数模板
2.3.4.1. 为什么有函数模板
当某几个函数算法相同但是参数的类型不同时,使用函数模板能够简化代码量,使程序更简洁
2.3.4.2. 函数模板是什么
函数模板就是将具体函数中的数据类型参数化—即用通用的参数取代函数中具体的数据类型,从而形成一个通用模板来代表数据类型不同的一组函数。
2.3.4.3. 函数模板怎么用
定义函数模板的语法如下:
template <class T1,class T2,…,class Tn>
返回值类型 函数名(用模板参数取代具体类型的形参列表)
{
用模板参数取代具体数据类型的函数体;
}
再用个实例说明下吧:
#include<stdio.h>
template<class T>
T max(T num1, T num2) //函数模板
{
T themax;
themax = (num1 >= num2) ? num1 : num2;
return themax;
}
int main() {
int x1;
double x2;
x1 = max(1, 2);
x2 = max(1.2, 2.2);
printf("%d %.3f", x1, x2);
return 0;
}
2.3.5. $lambda$函数(vc不支持新语法,所以不能运行)
2.3.5.1. 为什么要有$lambda$函数
用便捷的程序语句完成强大的函数功能,即简化语句。
2.3.5.2. $lambda$函数是什么
它本质上是一种匿名函数,能够简化代码量。
2.3.5.3. $lambda$函数怎么用
定义一个$lambda$函数的语法形式如下:[捕获列表] (参数列表)->返回值类型{函数体;};
其中:
- [捕获列表]:是一个可能为空的捕获列表,作用是指明定义$lambda$函数的作用域中的哪些对象(包括变量、常量等)可以在$lambda$函数中使用,以及它们在$lambda$函数中的存在方式是值的拷贝还是变量的引用。
- (参数列表):是$lambda$函数的参数列表,形式和普通函数的参数列表相同。如果一个$lambda$函数没有参数,则小括弧为空,也可以省略小括弧。
- ->返回值类型:$lambda$函数定义中的这部分内容表示函数的返回值类型,是可以省略的。因为编译器可以根据函数的内容自动推定函数的返回值类型。
- {函数体;}:花括弧中是$lambda$函数的函数体。
用实例说明下简单使用方法:
#include<stdio.h>
int main() {
auto f1 = [](int i, int j) { return i > j ? i : j; }; //定义了一个lambda函数
int max = f1(10, 15); //调用lambda函数,求两个值10和15中较大的一个
printf("%d", max);
return 0;
}
2.3.6. 可变长参数的函数
2.3.6.1. 为什么有可变长参数的函数
加强了程序的灵活性。
2.3.6.2. 可变长参数的函数是什么
$C++11$引入了定义和调用具有可变长参数函数的方法,这种函数可以接收可变数量的参数,且不同参数的数据类型也可以不同。
2.3.6.3. 可变长参数的函数怎么用
下面用一个递归程序来解释可变长参数的函数的用处:
#include<stdio.h>
template<typename T>
T sum(T t) { //递归终止条件
return t;
}
template<typename T, typename...Args>
T sum(T t, Args...args) { //递归拆分公式
return t + sum(args...);
}
int main() {
int ans = sum(1, 2, 3, 4, 5);
printf("%d", ans);
}
2.3.2. 函数的调用
2.3.2.1. 对参数的调用
对参数的调用分两种:($1$)传值调用($2$)引用调用。
- 传值调用:主调函数中的实际参数和被调函数中的相应的形式参数是相互独立的量——即各自具有自己的存储空间。当发生函数调用时,实参的值被拷贝给相应的形参,而后,连接它们的脐带就被剪断,所以当被调函数中形参的值改变时,主调函数中的实参不会随之改变 。
- 引用调用:引用类型的形式参数是相应的实际参数的别名,它们其实是同一个变量。所以如果在被调函数中形参的值发生了改变,则主调函数中实参的值也会随之改变 。
用实例说明下吧:
#include<stdio.h>
#include<string.h>
struct Student {
char name[30] = "";
bool is_hungry = true;
};
void fake(Student Sam) { //传值调用,只是拷贝
Sam.is_hungry = false;
}
void real(Student& Sam) { //引用调用,在原数据上修改
Sam.is_hungry = false;
}
int main() {
Student Sam; //Sam很饿
strcpy_s(Sam.name, "Sam");
fake(Sam);
printf("Sam只是在脑海中想了想吃东西, 他还是很饿: \n");
printf("%d\n", Sam.is_hungry);
printf("Sam到食堂去吃了东西, 他现在不饿了: \n");
real(Sam);
printf("%d\n", Sam.is_hungry);
return 0;
}
2.3.2.2. 对函数的调用(递归)
一个函数调用它本身就是递归。递归通常把一个大型复杂的问题层层转化为子问题,直到到子问题无需进一步递归就可以解决的地步。递归极大地降低了代码量。
通常来讲一个递归算法由以下部分组成:
• 能够不使用递归来产生答案的终止方案
• 可将所有其他情况拆分到基本案例
最常见的就是$Fibonacci$数列
$F0 = 0 ; F1 = 1; Fn = Fn-1 + Fn-2 , n$ $\geq$ $2$
比如说我们现在要求出$Fibonacci$数列的第5个数字,根据上文提到的递归的组成部分,我们将递归函数的组成分为两部分:
• 当传入的实参是$0$时返回$0$,当传入的实参时$1$时返回$1$(终止方案)
• 其他情况函数递归调用自己(拆分)
#include<iostream>
#include<unordered_map>
using namespace std;
unordered_map<int, int>hash1;
int Fibonacci(int n) {
if (n == 0) //终止方案
return 0;
if (n == 1) //终止方案
return 1;
if (hash1[n])
return hash1[n];
return hash1[n] = Fibonacci(n - 1) + Fibonacci(n - 2); //拆分 函数调用自己
}
int main() {
cout << Fibonacci(5);
return 0;
}
修改
- 伍树明 2021年10月16 2445806031@qq.com