前言
本文是基于中国MOOC平台上的《C语言程序设计进阶(翁恺)》课程,所作的一篇课程笔记,便于后期进行系统性查阅和复习。
一、指针与字符串
1.1 指针的使用
1.1.1 指针的使用:指针有什么用?
指针用于交换变量(交换两个变量的值)
void swap(int *pa,int *pb); int main(){ int a=2,b=3; swap(&a,&b); printf("%d%d",a,b); return 0; } void swap(int *pa,int *pb)//交换地址 { int t=*pa; *pa=*pb; *pb=t; }
指针用来函数返回多个值
•函数返回多个值,某些值就只能通过指针返回(函数只能返回一个值)
•传入的参数实际上是需要保存带回的结果的变量
指针用于函数返回状态
•函数返回运算的状态,结果通过指针返回。
(状态用函数的return来返回,而实际的值通过指针参数来返回)
•常用的套路是:让函数返回特殊的不属于有效范围内的值来表示出错。
-1或0(在文件操作会大量的例子,方便做if语句)
•但是当任何数值都是有效的可能结果,就得分开返回了
•在C语言中,只有这种方式来解决这一问题。而在C++、Java中采取了异常机制来解决这个问题。
指针应用场景:运算可能出错
两个整数做除法的函数
#include<stdio.h> int divide(int a, int b, int* result); int main() { int a = 5, b = 3, c; if (divide(a, b, &c)) { printf("%d/%d=%d", a, b, c); } return 0; } int divide(int a, int b, int* result) { int ret = 1; if (b == 0) ret = 0; else { *result = a / b;//相除的结果通过指针result返回 } return ret;//相除的状态通过函数的return返回 }
初学者常见错误:未初始化指针
•定义了指针变量,还没有指向任何变量,就开始使用指针。
int *p; int i=12; *p=12; //错误操作
•在这种情况下,*p指向的地址是未知的,可能那个地址是只读不可写的,那么常用填入12程序就会报错。
int *p; int i=12; p=&i; //正确操作
1.1.2 指针与数组:为什么数组传进函数后的sizeof不对了
通过函数参数把数组传入到函数中,函数参数接收到的是什么?
•传一个普通变量,参数接收到的是值;传一个指针,参数接收到的是值(这个值是个地址)
•传一个数组,参数接收到的什么?
函数参数表中的数组实际上是指针,sizeof(a)==sizeof(int*)
但可以用数组的运算符[]进行运算
•以下四种函数原型等价
int sun(int *ar,int n);
int sum(int ar[],int n);
int sum(int *,int);
int sum(int [],int);
数组变量是特殊的指针
•数组变量本身表达地址,所以无需&取地址
int a[10]; int *p=a;//无需&取地址
•但是数组的单位表达的是变量,需要用&取地址
int a[10];int *p=&a[3];//需要&取地址
a==&a[0];
•[]运算符可以对数组做,也可以对指针做:p[0]
对指针p做p[0],是什么意思?
如果我以为p所指的地方是个数组的话,那么如果那个位置实际上是个变量,就可以把这个变量视为p[0],即它所指的那个地址吧的第一个整数取出来作为p[0]。
•*运算符可以对指针做,也可以对数组做:*a=a[0]
•数组变量是const的指针,所以不能被赋值:int a[]==int *const a//两数组之间不能直接赋值
1.1.3 指针与const:指针本身和所指的变量都可能const
指针由两部分:指针本身,指针所指的变量
•判断两者哪个被const的标志:const在*的前面or后面。
前面——指针所指的变量是const;后面——指针本身是const。
•int const* p=&i;//指针所指的变量是const
const int *p=&i;//指针所指的变量是const
int *const p=&i;//指针本身是const
指针本身是const
•表示一旦得到某个变量的地址,不能再指向其他变量
int *const q=&i;//指针q是const
*q=26; //OK,指针q依然指向变量i,变量i的值发生了改变
q++; //ERROR,企图改变指针q指向的地址,发生错误
指针所指的变量是const
•表示不能通过这个指针去改变这个变量(并不能使得这个变量成为const)
const int *p=&i;
*p=24; //ERROR,不能通过指针p改变变量i
i=24; //OK
p=&t; //OK
转换
•总是可以把一个非const的值转换为const
void f(const int* x); int a=25; f(&a);//OK const int b=a; f(&b);//OK b=a+1;//ERROR
•用处:当要传递的参数类型比地址大的时候,这是常用手段。既能用比较少的字节数传递值给参数,又能避免函数对外面的变量进行修改。
const 数组
•const in a[]={1,2,3,4};
•数组变量已经是const的指针,这里的const表明数组的每个单位都是const int,所以必须通过初始化进行赋值。
保护数组值
•因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值。
•为了保护数组不被函数破坏,可以设置参数为const
int sum(const int a[],int len);
1.2 指针运算
1.2.1 指针运算
指针+1?
int i = 6; int* p = &i; printf("%p\n", p); //0000002EED32F594(地址是16进制) printf("%p\n",p+1); //0000002EED32F598
可见,p与p+1之间相隔一个sizeof(int)=4
int i = 6; char* p = &i; printf("%p\n", p); //00000068A50FF904 printf("%p\n",p+1); //00000068A50FF905
可见,p与p+1之间相隔一个sizeof(char)=1
ps:这里的sizeof()与不同类型数值占据字节数不同有关。
指针与数组
char ac={1,2,3,4,5,6,7}; char *p=ac; printf("%p\n", *p); //1 printf("%p\n",*(p+1)); //3
• *p <——> ac[0]
*(p+1) <——> ac[1]
•给指针加1表示要让指针指向下一个变量。
•如果指针不是指向一片连续分配的空间,如数组,则这种运算没有意义。
指针运算
•有这些算术运算可以对指针做:
•给指针加、减一个整数(+,+=,-,-=)
•递增递减(++,--)
•两指针相减(必须要同一类型指针,同为char型、同为int型)
int ac[] = { 1,2,3,4 }; int* p = ac; int* p1 = &ac[2]; printf("%p\n", p); //000000E531DFF778 printf("%p\n", p1); //000000E531DFF780 printf("%p\n", p1 - p); //0000000000000002
可见,&ac[2]与&ac[0]之间相隔了8字节,两指针相减结果为(&ac[2]-&ac[0])/sizeof(int)
*p++
•取出指针p所指位置的那个数据来,完事之后顺便把p移到下一个位置去
•*的优先级虽然高,但没有++高
•常用于数组类的连续空间操作
•在某些CPU上,这可以直接被翻译成一条汇编指令
指针比较(地址大小的比较)
•<,<=,==,>,>=,!=都可以对指针做
•比较它们在内存中的地址
•数组中的单位的地址肯定是线性递增的
0地址
•当你的内存中有0地址,但是0地址通常是不能随便碰的地址,所以指针不应该接触0地址
•往往用0地址来表示特殊的事情:
返回的指针是无效的
指针没有被真正初始化(先初始化为0)
•NULL是一个预定定义的符号,表示0地址(有的编译器不愿意你用0来表示0地址)
指针的类型
•无论指向什么类型,所有的指针大小都是一样的,因为都是地址
•但是指向不同类型的指针是不能直接互相赋值的
•这是为了避免用错指针
指针的类型转换(初学时不要轻易尝试)
•void*表示不知道指向什么东西的指针(这是个指针,但不确定它指向什么,所以在这里先说它指向的是void。它指向了一块内存空间,空间内是什么我是不知道的。)
• 指针也可以转换类型
•int *p=&i;void *q=(void*)p;
•并没有改变p所指的变量的类型,而是让后人用不同的眼光通过p看它所指的变量。
•i本身还是个int,但我现在不当你是int,我认为你是个void!
用指针来做什么
•需要传入较大的数据时用作参数
•传入数组后对数组做操作
•函数返回不止一个结果
•动态申请内存
1.2.2 动态内存分配
输入数据
•如果输入数据,先告诉你个数,然后再输入,要记录每个数据
•C99可以用变量做数组定义的大小,C99之前呢?
#include <stdio.h> int main() { int n; // 定义数组大小n printf("请输入数组元素个数:"); scanf("%d", &n); int arr[n]; // 根据输入的大小创建动态数组 for (int i = 0; i < n; ++i) { printf("请输入第%d个元素:", i+1); scanf("%d", &arr[i]); } return 0; }
•C99之前怎么做到的?
#include <stdio.h> #include <stdlib.h> // 包含malloc函数所需要的头文件 int main() { int n; // 定义数组长度n printf("请输入数组长度:"); scanf("%d", &n); int *arr = (int *) malloc(sizeof(int) * n); // 根据数组长度动态分配内存空间 if (arr == NULL) { printf("内存分配失败!\n"); return -1; } for (int i = 0; i < n; ++i) { scanf("%d",&a[i]); // 初始化数组元素 } free(arr); // 释放已经分配的内存空间 return 0; }
malloc
#include<stdlib.h>
void*malloc(size_t size);
•向malloc申请的空间大小是以字节为单位的
•返回的结果是void*,需要类型转换为自己需要的类型
(int*)malloc(n*sizeof(int));//类型转换为int
没空间了?(没有足够的内存分配下来)
•如果申请失败则返回0,或者叫做NULL
•你的系统能给你多大的空间?
#include <stdio.h> #include<stdlib.h> int main() { void* p; int cnt = 0; while ((p = malloc(100 * 1024 * 1024))) {//1.对p进行赋值。2.p值作为while循环的条件 cnt++; } printf("分配了%dMB的空间\n", cnt); return 0; }
free()
•把申请的来的空间还给“系统”
•申请过的空间,最终都应该还
•只能还申请来的空间的首地址
常见问题:
•申请了没free——>长时间运行内存逐渐下降(服务器程序一直在运行)
新手:忘了
老手:找不到合适的free时机
• free过了再次free
•地址变过了,直接去free
建议:
•一旦有malloc就对应一个free,一一对应不要忘记
•对程序的整体架构进行良好设计,保障程序有恰当的时机去free
•多阅读优秀代码,实际出真知
1.3 字符串操作
1.3.1 单字符输入输出
方式一:scanf函数输入,printf函数输出。
其实在学习scanf函数和printf函数时,就已经知道了一种单字符输入输出的方式:
#include<stdio.h> int main() { char c; scanf_s("%c", &c); printf("%c", c); return 0; }
方式二:getchar函数输入,putchar函数输出。
这种方式和方式一,都是C语言中常用的单字符输入输出方式。个人理解,安排在这里更多是为后面学习计算机底层逻辑的学习埋下伏笔。
#include<stdio.h> int main() { char ch = getchar(); putchar(ch); return 0; }
一些底层运行讲解(暂时看不太懂,不知道如何做记录,留白)
1.3.2 字符串输入输出
方式一:scanf函数输入,printf函数输出。
#include<stdio.h> int main(){ char str[100]; scanf("%s", str); printf("%s",str); return 0; }
方式二: fgets函数输入
fgets函数读取的字符串包括换行符(
\n
)。如果fgets成功读取到数据,则返回一个指向字符串的指针,否则返回空指针。#include<stdio.h> int main(){ char str[100]; fgets(str, sizeof(str), stdin); printf("%s",str); return 0; }
1.3.3 字符串数组的输入输出
字符串数组(写一个数组去表达很多个字符串),该如何写?
•想法一(×):char **a。a是一个指针,指向另一个指针,而那个指针指向一个字符(串)。这种方式可以在运行时动态地分配和调整数组的大小,但需要更多的内存和处理开销。
•想法二(×):char a[][]二维字符数组。a是一个二维数组的这个变量,可是在二维数组定义中,第二维一定要有确切的大小。如char a[][10],a是个数组,a这个数组里的每一个单元是一个char [10](a[0]是一个char[10])。这样看起来没问题,但需要确保输入的每一个字符串都不超过char[10]的范围,不够灵活。
•想法三(√):char *a[]字符指针数组。a是一个指针,指向一个数组。这种方式能够灵活地添加、删除和修改数组中的字符串。
方式一:char **a
方式二:char a[][]
方式三:char *a[]
1.4 字符串函数的实现
这些字符串函数都在一个头文件#include<string.h>里面
#include<string.h>
strlen,strcmp,strcpy,strcat,strchr,strstr
1.4.1 函数strlen
size_t strlen(const char*s);
•返回s的字符串长度(不包含结尾的\0)
char line[] = { "Hello" }; printf("strlen=%lu\n", strlen(line));//strlen=5 printf("sizeof=%lu", sizeof(line)); //sizeof=6
1.4.2 函数strcmp
int strcmp(const char*s1,const char *s2);
•比较两个字符串,返回:
0 : s1==s2
正数: s1>s2
负数: s1<s2
1.4.3 函数strcpy
char *strcpy(char *restrict dst,const char *restrict src);
•把src的字符串拷贝到dst的空间中
•restrict表明src和dst不重叠(C99)
•返回dst(让strcpy的结果能够参与其他运算)
常用方式:复制一个字符串
charr *dst=(char*)malloc(strlen(src)+1);//+1是给\0使用的,不要忘记!
strcpy(dst,src);
1.4.4 字符串搜索函数
在字符串中找字符strchr,strrchr
•char *strchr(const char*s,int c);//在char *s中找到c第一次出现的位置,从左边数过来第一次出现的位置,返回的是指针
•char *strrchr(const char*s,int c);//从右边数过来
•返回NULL表示没有找到
在字符串中找字符串
•char *strstr(const char *s1,const char *s2);
•char *strcasestr(const char *s1,const char *s2);
二、ACLLib的基础图形函数
本章内容不在该文中,另有文章进行记录,随后会添加链接。(未完待续)
三、结构类型
3.1 枚举
常量符号化
•用符号,而不是用具体的数字来表示程序中的数字。
•好处:增大程序代码的可读性。
const int green=0; const int yellow=1;
枚举
•用枚举,而不是定义独立的const int变量。
•枚举是一种用户定义的数据类型,它用关键词enum以如下语法来声明:
enum 枚举类型名字{名字0,...,名字n};
•枚举类型名字通常并不真的使用,要用的是在大括号里的名字,因为它们就是常量符号,它们的类型是int,值则从0到n。如:
enum colour{red,yellow,green};
//就创建了三个常量:const int red=0,const int yellow=1,const int green=2
•当需要一些可以排列起来的常量值时,定义枚举的意义就是给这些常量值名字。
•枚举实际上就是在声明一种新的数据类型,这种数据类型叫做枚举类型名字。
•声明过后,可以把这个枚举类型名字当作像int、float这样的数据类型来用。如:
enum colour{red,yellow,green}; void f(enum colour c);//不要把enum给忘掉了
套路:自动计数的枚举
•最后的那个值是前面的计数,green=2,表示前面有两个数值。
•在所有有意义的名字的后面放一个number,用这个number表示说枚举量里面有多少个值。
•这样需要遍历所有的枚举量或者需要建立一个用枚举量做下标的数组的时候就很方便了。
枚举量
•声明枚举量的时候可以指定值
•enum color{red=1,yellow,green=5};//red=1,yellow=2,green=5
枚举只是int
•即使给枚举类型的变量赋不存在的整数值,也没有任何的warning或error。
•虽然枚举类型可以当作类型使用,但是实际上不好用。
•如果有意义上排比的名字,枚举比const int方便。
•枚举比宏(宏没有类型)好,因为枚举有int类型。
•C语言中枚举主要用于定义符号量,而不是把它当作一个枚举类型来使用。
3.2 结构
3.2.1 结构类型
•一个结构就是一个复合的数据类型,在里面可以有很多各种类型的它的成员,然后用一个变量来表达这么多的数据。
struct date { int month; int day; int year; };//不要漏掉这个分号!!!! struct date today; today.month = 07; today.day = 31; today.year = 2014; printf("Today's date is %i-%i-%i.\n", today.year, today.month, today.day); //输出Today's date is 2014-7-31.
在函数内外?
•和本地变量一样,在函数内部声明的结构类型只能在函数内部使用
•所以通常在函数外部声明结构类型,这样就可以被多个函数所使用。
声明结构的形式(三种形式)
struct point { int x; int y; };//声明一个结构类型 struct point p1, p2;//定义了两个结构变量p1,p1,这两个变量的类型是struct point
•p1和p2都是point,里面有x和y的值。
struct{ int x; int y; }p1, p2; //这种情况就是:想要两个变量,这两个变量里面有x和y是明确的,可是它并不希望在后面继续用到这种结构类型,所以他没有给这个类型取名字,只是需要p1和p2这两个变量。(这种做法并不常见)
•p1和p2都是一种无名结构,里面有x和y。
struct point{ int x; int y; }p1, p2;
•p1和p2都是point,里面有x和y的值。
结构变量
• struct date today;
today.month = 11;
today.day = 23;
today.year = 2007;
结构的初始化
•初始化的两种方式
struct date today={07,31,2014}; struct date today={.month=7,.year=2014};
•在上述的第二种方式中,没有对today.day赋值,默认today.day=0。
结构与数组的异同
•数组里有很多个单元,但数组的单元必须是相同类型。
•结构里有很多个成员,但结构的成员可以是不同类型。
•数组用[]运算符和下标访问其成员,a[0]=10;
•结构用 .运算符和名字访问其成员,today.day,p1.x,p2.y
结构运算
•要访问整个结构,直接用结构变量的名字
•对于整个结构,可以做赋值、取地址,也可以传递给函数参数
p1=(struct point){5,10};//相当于p1.x=5,p1.y=10,(struct point)意为强制类型转换
p1=p2;//相当于p1.x=p2.x,p1.y=p2.y
结构指针
•和数组不同,结构变量的名字并不是结构变量的地址,必须使用&运算符
struct data *pDate=&today
scanf("%s",&today);
3.2.2 结构与函数
结构作为函数参数
int number0fDays(struct date d)
•整个结构可以作为参数的值传入函数
•这时候是在函数内新建一个结构变量,并复制调用者的结构的值
•也可以返回一个结构
(这些都与数组不同)
输入结构
•没有直接的方式可以一次scanf一个结构
#include <stdio.h> struct date { int month; int day; int year; }; int main() { struct date today; scanf("%i %i %i", &today.month, &today.day, &today.year);//读入结构 return 0; }
•方案一:把一个结构传入了函数,然后在函数中操作,但是没有返回回去。
(这与前面出现的交换函数陷阱类似,参数传递是值)
记住:C在函数调用时是传值的
#include <stdio.h> struct point { int x; int y; }; void getStruct(struct point); void output(struct point); int main() { struct point y = { 0,0 }; getStruct(y); output(y); return 0; } void getStruct(struct point p) { scanf_s("%d", &p.x); scanf_s("%d", &p.y); printf("%d %d\n", p.x, p.y); } void output(struct point p) { printf("%d %d\n", p.x, p.y);//在函数读入p值之后,并没有任何东西回到main,所以y={0,0} }
•方案二:在这个输入函数中,完全可以创建一个临时的结构变量,然后把这个结构返回给调用者。
#include <stdio.h> struct point { int x; int y; }; struct point getStruct(void); void output(struct point); int main() { struct point y = { 0,0 }; y=getStruct(); output(y); return 0; } struct point getStruct(void) { struct point p; scanf_s("%d", &p.x); scanf_s("%d", &p.y); printf("%d %d\n", p.x, p.y); return p; } void output(struct point p) { printf("%d %d\n", p.x, p.y); }
•在传一个结构给函数的方式中,最优解:不传结构(费时费空间),而是传结构的指针。
指向结构的指针
struct date { int month; int day; int year; }myday; struct date *p = &myday; (*p).month = 12; p->month = 12;//这两个式子是一样的意思
•用->表示指针所指的结构变量中的成员
•用指针解决结构传出函数的问题
#include <stdio.h> struct point { int x; int y; }; struct point* getStruct(struct point *p); void output(struct point); int main() { struct point y = { 0,0 }; getStruct(&y); output(y); return 0; } struct point* getStruct(struct point* p){//指针函数 scanf_s("%d", &p->x); scanf_s("%d", &p->y); printf("%d %d\n", p->x, p->y); return p; } void output(struct point p) { printf("%d %d\n", p.x, p.y); }
3.2.3 结构中的结构
结构数组(与int、float类似)
struct date dates[100]; struct date dates[] = { {4,5,2007},{2,4,2009} };//二维数组
#include <stdio.h> struct time { int hour; int minutes; int seconds; }; int main() { struct time testTimes[5] = { {4,5,11},{2,4,50},{2,45,45},{4,37,28},{8,22,9} }; return 0; }
结构中的结构
struct point { int x; int y; }; struct rectangle { struct point pt1; struct point pt2; };
•如果有变量:struct rectangle r;
就可以有:r.pt1.x、r.pt1.y、r.pt2.x、r.pt2.x
•如果有变量:struct rectangle r.*rp;
rp=&r;
那么下面的四种形式是等价的:r.pt1.x
rp->pt1.x
(r.pt1).x
(rp->pt1).x
但是没有rp->pt1->x(因为pt1不是指针)
结构的数组
struct point { int x; int y; }; struct rectangle { struct point pt1; struct point pt2; }; struct rectangle recs[] = { {{1,2},{3,4}}, {{4,5},{7,8}} };//2rectangle
3.3 联合
3.3.1 类型定义
•结构类型:通过struct这个关键字去声明一个结构类型,然后用这个结构类型去定义变量。
typedef(自定义数据类型)
•C语言提供了一个叫做typedef的功能来声明一个已有的数据类型的新名字。
如typedef int length;
使得length成为int类型的别名。
•如此,length这个名字就可以代替int出现在变量定义和参数声明的地方。
length a,b,len;
length number[10];
• 那么使用length可以简化结构的复杂名字,改善程序的可读性。
typedef long int64_t;//重载已有的类型名字,新名字的含义更清晰,具有可移植性 typedef struct ADate { int month; int day; int year; }Date;//简化复杂的名字 int64_t i = 10000; Date d = { 9,8,2019 };
3.3.2 联合
union与struct
•相同点:程序样式
union point { int a; char b; }elt1, elt2; elt1.a = 4; elt1.b = 'c'; elt2.a = 0xDEADBEEF;
•不同点:struct中两个成员的值是分开储存的,但union中这两个成员实际上占据了相同的内存空间(联合起来使用相同的空间)。
联合
•存储
所有成员共享一个空间。
同一时间只有一个成员是有效的。
union的大小是最大的成员。
•初始化
对第一个成员做初始化
•union的常用方式:通过union得到一个数内部的各个字节。
#include <stdio.h> typedef union { int i; char ch[sizeof(int)]; }CHI; int main() { CHI chi; int i; chi.i = 1234; for (i = 0; i < sizeof(int); i++) { printf("%02hhX", chi.ch[i]);//输出D2040000,十六进制,低位在前 } return 0; }
四、链表
本周内容用于了解C语言的某些较为复杂的应用。
4.1 可变数组
4.1.1 可变数组
C语言的数组是固定大小的,尽管C99 标准中可以用变量来定义一个数组的大小,但一旦运行其大小就不能再改变。想办法实现一个可变大小的数组,该数组应有以下特点:
•growable
•get the current size
•access to the elements
实现一个函数库,定义一些函数,这些函数可以给我们提供一种自动增长的数组
•Array array_create(int init_size); //创建一个数组
•void array_free(Array *a); //用于回收空间
•int array_size(const Array *a); //告诉我们数组里有多少单元
•int *array_at(Array *a,int index); //用于访问数组中的单元
•void array_inflate(Array *a,int more_size);//让数组长大
头文件array.h
#ifndef _ARRAY_H_ #define _ARRAY_H_ typedef struct { int* array; int size; }Array;//数组 Array array_create(int init_size); void array_free(Array* a); int array_size(const Array* a); int* array_at(Array* a, int index); void array_inflate(Array* a, int more_size); #endif
•这里有一个不推荐用法,但在有些代码中可能会遇到(可读性差,不要写)。
typedef struct { int* array; int size; }*Array; Array a;//那么这个a,其实是个指针
源文件array.c
#include <stdio.h> #include <stdlib.h> #include"array.h" //创建一个数组 Array array_create(int init_size) { Array a; a.size = init_size; a.array = (int*)malloc(sizeof(int) * a.size); return a; } //回收空间 void array_free(Array* a) { free(a->array); a->array = NULL; a->size = 0; } //可变数组的数据访问 //封装 int array_size(const Array* a) { return a->size; } int* array_at(Array* a, int index) { return &(a->array[index]); } //可变数组的自动增长 void array_inflate(Array* a, int more_size) { int* p = (int*)malloc(sizeof(int) * (a->size + more_size)); int i; for (int i = 0; i < a->size; i++) { p[i] = a->array[i]; } free(a->array); a->array = p; a->size += more_size; } int main(int argc,char const *argv[]) { return 0; }
4.1.2 可变数组的数据访问
//可变数组的数据访问 int array_size(const Array* a) { return a->size; } int* array_at(Array* a, int index) { return &(a->array[index]); }
上述代码是一个可变数组的数据访问函数的实现。
array_size
函数返回数组的大小(size)。在函数内部,它简单地返回数组结构体Array
的size
成员的值。
array_at
函数返回指向数组中指定索引位置的值的指针。在函数内部,它返回了指向数组结构体Array
的array
成员中指定索引位置的指针。注意,这些函数都需要一个指向
Array
结构体的指针作为参数。这可以使函数能够访问并操作数组结构体的成员。
4.1.3 可变数组的自动增长
//可变数组的自动增长 void array_inflate(Array* a, int more_size) { int* p = (int*)malloc(sizeof(int) * (a->size + more_size)); int i; for (int i = 0; i < a->size; i++) { p[i] = a->array[i]; } free(a->array); a->array = p; a->size += more_size; }
这段代码实现了一个可变数组的自动增长的函数array_inflate。函数接受两个参数,一个是可变数组a,另一个是要增加的大小more_size。
1、通过调用malloc函数分配一个新的数组p,大小为当前数组a的大小加上增加的大小more_size,即sizeof(int) * (a->size + more_size)。这里使用了强制类型转换将分配的内存指针转换为int类型的指针。
2、使用一个for循环将当前数组a中的元素逐个复制到新数组p中,以保留原数组的内容。
接下来,调用free函数释放原来数组a所占用的内存。
3、将新数组p赋值给数组a的array成员,并更新数组a的size成员为原来的大小加上增加的大小,即a->size += more_size。
4、这样就实现了可变数组的自动增长,通过调用array_inflate函数,可变数组a会自动增加指定大小的空间。
4.2 链表
4.2.1 可变数组的缺陷
可变数组的缺陷
•每次数组进行自动增长时候,都要去申请一块新的内存空间(可以容纳下新的东西),然后再拷贝。这就导致了两个问题:
一、拷贝要花时间,数组越大耗时越长。
二、明明有足够的内存,却再也不能申请空间了。(往往出现在内存受限的场合,如单片机)
解决方式:
•在自动增长时,原来的那块内存不动,不去申请一块更大的内存,而是就申请一块block那么大的内存。把原来那块和新申请的内存链接起来,这样就不需拷贝。只需在原来那块内存用完后转到新内存块进行存储。
•如此就能解决拷贝耗时和内存块不连续的问题。
4.2.2 链表
链表的构造
链表与可变数组的区别(上,可变数组;下,链表)
节点的定义(node.h)
#ifndef _NODE_H_ #define _NODE_H_ typedef struct _node { int value; struct _node* next; }Node; #endif
链表的添加操作、遍历
#include<stdio.h> #include "node.h" #include<stdlib.h> int main(int argc, char const* argv[]) { int number; Node* head = NULL; scanf_s("%d", &number); do { if (number != -1) { //add number to linked-list Node* p = (Node*)malloc(sizeof(Node)); p->value = number; p->next = NULL; //find the last(遍历) Node* last = head; while (last->next) { last = last->next; } //attach last->next = p; free(p); } } while (number != -1); return 0; }
链表的特殊情况处理 (head=NULL)
这种情况下last本身就是NULL,判断last->next是无用的
#include<stdio.h> #include "node.h" #include<stdlib.h> int main(int argc, char const* argv[]) { int number; Node* head = NULL; scanf_s("%d", &number); do { if (number != -1) { //add number to linked-list Node* p = (Node*)malloc(sizeof(Node)); p->value = number; p->next = NULL; //find the last(遍历) Node* last = head; if (last) { while (last->next) { last = last->next; } //attach last->next = p; } else { head = p; } } } while (number != -1); return 0; }
4.2.3 链表的函数
将上一节中的if条件语句的函数体拿出来作为,一个链表的函数
#include<stdio.h> #include"node.h" #include<stdlib.h> void add(Node* head, int number); int main(int argc,char const *argv[]) { Node* head = NULL; int number; do { scanf("%d", &number); if (number != -1){ add(head, number); } } while (number != -1); return 0; } void add(Node* head, int number) { //add number to linked-list Node* p = (Node*)malloc(sizeof(Node)); p->value = number; p->next = NULL; //find the last Node* last = head; if (last) { while (last->next) { last = last->next; } //attch last->next = p; } else { head = p; } }
这段代码有个问题就是,Node* head一个指向Node类型的指针的函数参数。head=p这个操作是无效的,这里就又要提到函数参数的按值传递。
当我们将一个指针作为参数传递给函数时,实际上是将该指针的副本传递给函数。因此,当我们在函数内部修改这个指针的值时,实际上是在修改该副本,而不是原始指针本身。
在函数内部执行
head = p;
操作时,实际上是修改了局部变量head
的值,而不是原始指针head
的值。因此,当函数返回时,原始指针head
的值不会被改变。
方案一:设置全局变量Node* head。
(可以,但全局变量是对代码是有害的,应该避免使用全局变量)
方案二:Node* add(Node* head, int number) 则表示一个返回Node指针的函数,它的作用是将一个整数number添加到链表的末尾,并返回新的链表头节点的指针。
(可以,但对使用add函数的程序员不友好,需要head=add(head,number),易错)
方案三:使用指针的指针 void add(Node** head, int number)
#include<stdio.h> #include"node.h" #include<stdlib.h> void add(Node** head, int number); int main(int argc, char const *argv[]) { Node* head = NULL; int number; do { scanf("%d", &number); if (number != -1) { add(&head, number); } } while (number != -1); return 0; } void add(Node** head, int number) { //add number to linked-list Node* p = (Node*)malloc(sizeof(Node)); p->value = number; p->next = NULL; //find the last linked-list Node* last = *head; if (last) { while (last->next) { last = last->next; } //attch last->next = p; } else { *head = p; } }
方案四:使用自定义数据结构
好处在于,我们用了一种自定义的数据结构list来代表整个链表。现在只在该数据结构中放入了一个head,但是可以扩充的,很方便。
#include<stdio.h> #include"node.h" #include<stdlib.h> typedef struct _list { Node* head; }List; void add(List* pList, int number); int main(int argc, char const* argv[]) { List list; int number; list.head = NULL; do { scanf_s("%d", &number); if (number != -1) { add(&list, number); } } while (number != -1); return 0; } void add(List* pList, int number) { //add number to linked-list Node* p = (Node*)malloc(sizeof(Node)); p->value = number; p->next = NULL; if (pList->head == NULL) { //empty list, set head to new node pList->head = p; } else { //find the last linked-list Node* last = pList->head; while (last->next) { last = last->next; } //attach last->next = p; } }
4.2.4 链表的搜索
前面我们已经实现了将number存入链表,那么如何完成对链表的其他操作?
链表的输出操作
//链表的输出操作(遍历链表) void print(List* plist) { Node* p; for (p = plist->head; p; p = p->next) { printf("%d\t", p->value); } }
4.2.5 链表的删除
链表的删除操作(在链表中找number,并删除number)
void cut(int number, List* plist) { Node* p; Node* q; for (q=NULL,p = plist->head; p; q=p,p = p->next) { if (p->value == number) { if (q) { q->next = p->next; } else { plist->head = p->next; } free(p); break; } } }
观察上述代码,发现都有if语句判断指针是否为NULL
原因: 指针的左边必须被判断不为NULL,如
if (last) { while (last->next) { last = last->next; } //attch last->next = p; } else { pList->head = p; }
但也有特殊情况,不需要判断。
不过该代码在VS2022 依然会报错显示:C6011:取消对NULL指针"p"的引用。这因为动态分配内存空间可能会出现错误。
(这段代码在分配内存时使用了malloc函数,该函数返回的是指向分配内存空间的指针。只有在内存分配失败时,malloc函数才会返回NULL。因此,如果malloc函数成功分配了内存空间,则p不会为NULL。因此,不需要排除p为NULL的可能性。)
struct{ int head; }Node; Node* p = (Node*)malloc(sizeof(Node)); p->head = number;
4.2.6 链表的清除
链表的清除操作
void clean(List* plist) { Node* p; Node* q; for (p = plist->head; p; q = p) { q = p->next; free(p); p = NULL; } }
五、程序结构
5.1 全局变量
5.1.1 全局变量:定义在函数之外的变量,全局的生存期和作用域
•定义在函数外面的变量是全局变量
•全局变量具有全局的生存期和作用域
(它们与任何函数都无关,在任何函数内部都可以使用它们)
全局变量初始化
•没有做初始化的全局变量会得到NULL
(这里与本地变量是有区别的,本地变量没有被初始化,它的值将是未定义的)
•只能用编译时刻已知的值来初始化全局变量
•它们的初始化发生在main函数之前
被隐藏的全局变量
•如果函数内部存在与全局变量同名的变量,则全局变量被隐藏
5.1.2 静态本地变量:能在函数结束后继续保有原值的本地变量
静态本地变量
•在本地变量定义时加上static修饰符就成为静态本地变量
int main() { static int a;//静态本地变量 return 0; }
•当函数离开的时候,静态稳定变量会继续存在并保持其值
•静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数会保持上次离开的值
•静态本地变量实际上是特殊的全局变量(两者有相同的内存区域)
#include<stdio.h> int gAll = 12; int main() { int k = 0; static int all=1; printf("gAll=%p\n",&gAll ); printf("all=%p\n",&all ); printf("k=%p\n",&k ); return 0; }
gAll和all的地址相近
•静态本地变量具有全局的生存期,函数内的局部作用域
5.1.3 后记:返回指针的函数,使用全局变量的贴士
返回指针的函数int f(int *a);
•返回本地变量的地址是危险的
•返回全局变量或静态本地变量的地址是安全的(但不要这么做)
•返回在函数内malloc的内存是安全的,但容易造成问题
•最好的做法:返回传入的指针
tips
•不要使用全局变量来在函数间传递参数和结果
•尽量避免使用全局变量(丰田汽车暴冲事件)
•使用全局变量或静态本地变量的函数对于多线程的环境是不安全的
5.2 编译预处理和宏
5.2.1 宏的定义
编译预处理指令
•#开头的是编译预处理指令
•它们不是C语言的成分,但是C语言程序离不开它们
•#define用来定义一个宏
#define
•#define<名字><值>
•注意:没有结尾的分号,因为不是C的语言
•在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值
(完全的文本替换)
#define PI 3.14159 printf("%f\n",3*PI);//在运行时,编译器会直接看到PI那里是3.14159,因为已经文本替换了
宏
•如果一个宏的值中有其他宏的名字,也会被替换的
#define PI 3.14159 #define PI2 2*PI
•如果一个宏的值超过一行,最后一行之前的行的行末需要加\
#define PRT printf("%f",PI);\ printf("%f\n",PI2);
•宏的值后面出现的注释不会被当作宏的值的一部分
没有值的宏
•#define _DEBUG
•这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了
预定义的宏
•是在编程语言中提前定义好的一些宏(或者叫做预处理指令),可以用来在程序中进行一些特定操作或者获取一些已经定义好的信息。预定义的宏通常以特定的关键字或者符号表示,不同的编程语言可能有不同的预定义宏。下面是一些常见的预定义宏的示例:
- __LINE__:表示当前代码所在的行号。
- __FILE__:表示当前代码所在的文件名。
- __DATE__:表示当前代码编译时的日期。
- __TIME__:表示当前代码编译时的时间
- __STDC__:用于C和C++中,表示当前代码是否符合C语言标准。
这些预定义宏可以在程序中直接使用,例如可以通过__LINE__宏来打印当前代码所在的行号,或者通过__FILE__宏来获取当前代码所在的文件名等。预定义宏的具体定义和使用方式可以查阅相应编程语言的文档或者编译器手册。
5.2.2 带参数的宏
像函数的宏
•#define cube(x) ((x)*(x)*(x))//x是参数,这里的参数是没有类型的
•宏可以带参数
#define cube(x) ((x)*(x)*(x)) printf("%d\n",cube(5));//cube(5)被替换为((5)*(5)*(5))
带参数的宏的原则
•一切都要括号(整个值要括号,参数出现的每个地方都要括号)
•正确的宏
#define cube(x) ((x)*3.14159)
•错误的宏
#define cube(x) (x)*3.14159
#define cube(x) (x*3.14159)
带参数的宏
•可以带多个参数
#define MIN(a,b) ((a)>(b)?(b):(a))
•也可以组合(嵌套)使用其他宏
5.3 大程序结构
5.3.1 多个源代码文件
把函数放在其他源代码文件中
•分开前
#include<stdio.h> int max(int a, int b); int main() { int a, b; scanf_s("%d%d", &a, &b); printf("max=%d\n", max(a, b)); return 0; } int max(int a, int b) { return a > b ? a : b; }
•分开后
//main.c #include<stdio.h> int max(int a, int b); int main() { int a, b; scanf_s("%d%d", &a, &b); printf("max=%d\n", max(a, b)); return 0; } //max.c int max(int a, int b) { return a > b ? a : b; }
在分开后的源代码文件中,main.c保留了max.c的原型声明。如果不保留会什么样?
在C语言中,编译器会在编译max(a,b)猜测max()函数中所有的类型都是int,即int max(int a,int b)。这一猜测对于上述代码无害,但如果我的max()函数实际并非如此,就会出现错误。
5.3.2 头文件
在前面可以看到,在使用函数时,需要在main.c中保留原型声明。而在实际项目应用中,原型声明往往放在单独的头文件中,以提高代码的可读性、可维护性和可重用性。
//max.h int max(int a, int b); //main.c #include<stdio.h> #include"max.h" int main() { int a, b; scanf_s("%d%d", &a, &b); printf("max=%d\n", max(a, b)); return 0; } //max.c int max(int a, int b) { return a > b ? a : b; }
头文件
•把函数原型放在头文件(以.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能在编译器在编译的时候知道函数的原型。
#include
•#include是一个编译预处理指令,和宏一样,在编译之前就处理了
•它把那个文件的全部文本内容原封不动地插入到它所在的地方,所以不是一定要在.c文件的最前面#include
•#include有两种形式来指出要插入的文件
#include<>:
用于包含标准库的头文件,这些头文件通常在系统的标准库目录中。编译器会根据特定的搜索路径在系统标准库目录中进行查找。
#include""
用于:包含非标准库的头文件,这些头文件通常在项目的源文件目录或指定的目录中。编译器会首先在当前源文件目录中查找,如果找不到再去指定的目录中查找。
#include的误区
•#include不是用来引入库的
•stdio.h中只有printf的原型声明,printf的函数代码有另外的地方,某个.lib(Windows)和.a(Unix)中。
•现在的C语言编译器默认会引入所有的标准库
•#include <stdio.h>只是为了让编译器知道printf函数的原型,保证你调用时给出的参数是正确的类型。
头文件
•在使用和定义这个函数的地方都应该#include这个头文件
(使用的地方加上头文件,编译器会帮忙检查对这个函数的调用是否正确;
定义的地方加上头文件,编译器会帮忙检查原型声明和实际函数的定义是否一致。)
//max.c #include"max.h" int max(int a, int b) { return a > b ? a : b; }
•一般的做法:任何的.c都有对应的同名.h,把所有对外公开的函数的原型和全局变量的声明都放进去
静态函数和静态全局变量
•静态函数
在函数前面加上static,就使得它成为只能在所在的编译单位中使用的函数。
•静态全局变量
在全局变量前面加上static,就使得它成为只能在所在编译单位中使用的全局变量。
5.3.3 声明
声明和定义
•声明本身不会产生实际的可执行代码,它只是为了帮助编译器理解程序的结构和逻辑。
函数原型、变量声明、结构声明、宏声明、枚举声明、类型声明、inline函数
•定义是产生代码的东西
函数、全局变量
声明和头文件
•只有声明可以被放在头文件中(这是规则,不这样做会出错),否则会引发重定义错误
•重定义错误:意味着同一个实体被多次定义。编译器无法确定使用哪个定义,因此会报错。
•重复声明:同一个编译单元里,同名的结构不能被重复声明。
但如果头文件里有结构的声明,很难这个头文件不会在一个编译单元里被#include多次。那么就需要“标准头文件结构”,来避免重复声明。
标准头文件结构
#ifndef _LIST_HEAD #define _LIST_HEAD #include"node.h" typedef struct _list { Node* head; Node* tail; }List; #endif
首先,使用
#ifndef
和#define
来判断是否已经定义了_LIST_HEAD
宏。如果没有定义,则执行下面的代码,如果已经定义了,则跳过整个代码块。•运用条件编译和宏,保证这个头文件在一个编译单元只会被#include一次。
•#pragma once也能起到相同的作用,但不是所有的编译器都至此
六、交互图形设计
本章内容不在该文中,另有文章进行记录,随后会添加链接。(未完待续)
七、文件
该章内容,C语言如何做文件和底层操作。(未完待续)