C语言
一、C程序运行机制
1.1 C程序运行机制简述
-
编辑:比如编写我们的hello.c 文件, 就是 源代码
-
编译:将 hello.c 程序 翻译成 目标文件(hello.obj) // 在计算机底层执行
-
链接:将目标文件 hello.obj + 库文件 生成可执行文件 (MyProject01.exe) //在计算机底层执行
-
运行:执行 .exe文件, 得到运行结果
-
图解:
1.2 编译、链接和运行详解
1.2.1 什么是编译
- 有了C源文件,通过编译器将其编译成obj 文件( 目标文件)
- 如果程序没有错误,没有任何提示,但在Debug目录下会出现一个Hello.obj文件,该文件称为目标文件
1.2.2 什么是链接
- 有了目标文件(.obj文件),通过链接程序将其和运行需要的c 库文件链接 成exe文件( 可执行文件)
- 如果程序没有错误,没有任何提示,但在Debug目录下会出现一个项目名.exe文件,该文件称为可执行文件
- 为什么需要链接库文件呢? 因为我们的C程序中会使用 C程序库的内容,比如<stdio.h> <stdlib.h> 中的函数printf() system()等等, 这些函数不是程序员自己写的,而是C程序库中提供的,因此需要链接
- 链接后,生成的.exe 文件,比obj 文件大了很多
1.2.3 什么是运行
- 有了可执行的exe文件, 也称为可执行程序 (二进制文件),就可以在控制台下可以直接运行 exe文件
注意:对修改后的hello.c源文件需要重新编译链接,生成新的exe文件后,再执行,才能生效
- C程序源文件以“c”为扩展名
- C程序的执行入口是main()函数
- C语言严格区分大小写
- C程序由一条条语句构成,每个语句以“;”结束
- 大括号都是成对出现的,缺一不可
二、数据类型
2.1 常量
2.1.1 关键字
2.1.2 常量
- 常量是固定值,在程序执行期间不能改变。这些固定的值,又叫做字面量
- 常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量
- 常量的值在定义后不能进行修改
2.1.3 整型常量
-
整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意
-
示例:
85 /* 十进制 */ 0213 /* 八进制 */ 0x4b /* 十六进制 */ 30 /* 整数 */ 30u /* 无符号整数 */ 30l /* 长整数 */ 30ul /* 无符号长整数 */
-
C语言如何表示相应进制数
十进制 | 以正常数字1-9开头,如123 |
---|---|
八进制 | 以数字0开头,如0123 |
十六进制 | 以0x开头,如0x123 |
二进制 | C语言不能直接书写二进制数 |
2.1.4 浮点型常量
-
浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量
-
示例:
3.14159; //double 常量 314159E-5; // 科学计数法 3.1f; //float常量
2.1.5 字符常量
- 字符常量是括在单引号中,例如,'x' 可以存储在 char 类型的变量中。字符常量可以是一个普通的字符(例如 'x')、一个转义序列(例如 '\t')
- 字符常量举例说明:'X','Y','A','b','1','\t
2.1.5 常量的定义
-
使用 #define 预处理器
#define 常量名 常量值 #define PI 3.14 //定义常量 PI 常量值3.14 int main() { //PI = 3.1415 可以吗? double area; double r = 1.2;//半径 area = PI * r * r; printf("面积 : %.2f", area); getchar(); return 0; }
-
使用 const 关键字
//可以使用 const 声明指定类型的常量 const 数据类型 常量名 = 常量值; const double PI = 3.14; int main() { //PI = 3.1415 可以吗? double area; double r = 1.2; area = PI * r * r; printf("面积 : %.2f", area); getchar(); return 0; }
-
const 和 和 #define 的区别
- const定义的常量时,带类型,define不带类型
- const是在 编译、运行的时候起作用,而define是在编译的预处理阶段起作用
- define只是简单的替换,没有类型检查。简单的字符串替换会导致 边界效应
- const常量可以进行调试的,define是不能进行调试的,主要是预编译阶段就已经替换掉了,调试的时候就没它了
- const不能重定义,不可以定义两个一样的,而define通过undef取消某个符号的定义,再重新定义
- define可以配合#ifdef、 #ifndef、 #endif 来使用, 可以让代码更加灵活,比如我们可以通过#define 来 启动或者关闭 调试信息
2.2 变量
2.2.1 变量的概念
-
变量相当于内存中一个数据存储空间的表示,你可以把变量看做是一个房间的门牌号,通过门牌号我们可以找到房间,而通过变量名可以访问到变量(值)
-
标识符命名规则:
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须为字母或下划线
- 标识符中字母区分大小写
-
变量特点:
- 变量在编译时为其分配相应的内存空间
- 可以通过其名字和地址访问相应内存
-
使用示例:
#include <stdio.h> void main() { int num = 1 ; //整型 double score = 2.3; //小数 char gender = 'A'; //字符 char name[] = "尚硅谷"; //字符串 //说明 //1. 如果输出的整数 %d //2. 如果输出的是小数 %f , 如果希望保留小数点 %.2f //3. 如果输出的是字符 %c //4. 如果输出的是字符串 %s //5. 在输出不同数据时,对应的格式化的 形式要对应起来 printf("num=%d score=%.2f gender=%c name=%s", num, score, gender, name); getchar(); }
-
变量使用注意事项
- 变量表示内存中的一个存储区域(不同的数据类型,占用的空间大小不一样)
- 该区域有自己的名称 和类型
- 变量必须先声明,后使用
- 声明和定义区别
- 声明变量不需要建立存储空间,如:extern int a;
- 定义变量需要建立存储空间,如:int b;
- 变量在同一个作用域内不能重名
- 变量三要素 (变量名+值+数据类型)
2.2.2 变量的数据类型
注意:
- 在c中,没有字符串类型,使用字符数组表示字符串
- 在不同的系统上,部分数据类型的字节长度不一样
2.3 整型
- 整型的类型
类型 | 存储大小 | 值范围 |
---|---|---|
char | 1字节 | -128 即-(2^7) 到 127 (2^7-1) |
unsigned char | 1字节 | 0 到 255 (2^8 - 1) |
signed char | 1字节 | -128 即-(2^7) 到 127 (2^7-1) |
int/signed int | 2或者4字节 | -32,768 (- 2^15 ) 到 32,767 (2^15-1)或 -2,147,483,648 (- 2^31) 到 2,147,483,647 (2^31 -1) |
unsigned int | 2或者4字节 | 0 到65,535 (2^16-1) 或0 到4,294,967,295 (2^32 -1) |
short/signed short | 2字节 | -32,768 (- 2^15)到 32,767 (2^15 -1) |
unsigned short | 2字节 | 0 到65,535 (2^16 - 1) |
long/signed long | 4字节 | -2,147,483,648 (- 2^31) 到 2,147,483,647 (2^31 - 1) |
unsigned long | 4字节 | 0 到4,294,967,295 (2^32 - 1) |
- 整型变量的输出格式
打印格式 | 含义 |
---|---|
%d | 输出一个有符号的10进制int类型 |
%o(字母o) | 输出8进制的int类型 |
%x | 输出16进制的int类型,字母以小写输出 |
%X | 输出16进制的int类型,字母以大写输出 |
%u | 输出一个10进制的无符号数 |
-
示例:
#include <stdio.h> int main() { int a = 123; //定义变量a,以10进制方式赋值为123 int b = 0567; //定义变量b,以8进制方式赋值为0567 int c = 0xabc; //定义变量c,以16进制方式赋值为0xabc printf("a = %d\n", a);//a = 123 printf("8进制:b = %o\n", b);//8进制:b = 567 printf("10进制:b = %d\n", b);//10进制:b = 375 printf("16进制:c = %x\n", c);//16进制:c = abc printf("16进制:c = %X\n", c);//16进制:c = ABC printf("10进制:c = %d\n", c);//有符号方式打印:d = -1 unsigned int d = 0xffffffff; //定义无符号int变量d,以16进制方式赋值 printf("有符号方式打印:d = %d\n", d);//有符号方式打印:d = -1 printf("无符号方式打印:d = %u\n", d);//无符号方式打印:d = 4294967295 return 0; }
注:C语言的整型类型,分为有符号 signed 和无符号 unsigned 两种,默认是 signed
2.4 浮点型
- 浮点型的分类
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float 单精度 | 4字节 | 1.2E-38 到 3.4E+38 | 6 位小数 |
double 双精度 | 8字节 | 2.3E-308 到 1.7E+308 | 15 位小数 |
注: 关于浮点数在机器中存放形式的简单说明,浮点数=符号位+指数位+尾数位 , 浮点数是近似值。尾数部分可能丢失,造成精度损失
- 注意事项
- 浮点型常量默认为double型 ,声明float型常量时,须后加‘f’或‘F’
- 浮点型常量有两种表示形式
- 十进制数形式:如:5.12 512.0f .512 (必须有小数点)
- 科学计数法形式:如:5.12e2 、 5.12E-2
- 通常情况下,应该使用double型,因为它比float型更精确。
2.5 布尔类型
- C语言标准(C89)没有定义布尔类型,所以C语言判断真假时以0为假,非0为真
- C语言标准(C99)提供了_Bool 型,_Bool仍是整数类型,但与一般整型不同的是,_Bool变量只能赋值为0或1,非0的值都会被存储为1,C99还提供了一个头文件<stdbool.h> 定义了bool代表_Bool,true代表1,false代表0。只要导入 stdbool.h ,就能方便的操作布尔类型了 , 比如 bool flag = false
2.6 字符类型(char)
-
基本介绍
字符类型可以表示单个字符 ,字符类型是char,char是1个字节(可以存字母或者数字),多个字符称为字符串,在C语言中 使用 char数组 表示,数组不是基本数据类型,而是构造类型
-
注意事项
- 在C中,char的本质是一个整数,在输出时,是 ASCII码对应
的字符 - 可以直接给char赋一个整数,然后输出时,会按照对应的
ASCII 字符输出
- 在C中,char的本质是一个整数,在输出时,是 ASCII码对应
-
字符类型本质
- 字符型存储到计算机中,需要将字符对应的码值(整数)找出来
- 存 储:字符'a'——>码值 (97)——>二进制 (1100001)——>存储()
- 读 取:二进制(1100001)——>码值(97)——> 字符'a'——>读取(显示)
- 字符型存储到计算机中,需要将字符对应的码值(整数)找出来
-
转义字符
转义字符 含义 ASCII****码值(十进制) \a 警报 007 \b 退格(BS) ,将当前位置移到前一列 008 \f 换页(FF),将当前位置移到下页开头 012 \n 换行(LF) ,将当前位置移到下一行开头 010 \r 回车(CR) ,将当前位置移到本行开头 013 \t 水平制表(HT) (跳到下一个TAB位置) 009 \v 垂直制表(VT) 011 \\ 代表一个反斜线字符"" 092 \' 代表一个单引号(撇号)字符 039 \ " 代表一个双引号字符 034 ? 代表一个问号 063 \0 数字0 000 \ddd 8进制转义字符,d范围0~7 3位8进制 \xhh 16进制转义字符,h范围09,af,A~F 3位16进制 注意:加粗字体标注的为不可打印字符。
2.7 sizeof关键字
- sizeof不是函数,所以不需要包含任何头文件,它的功能是计算一个数据类型的大小,单位为字节
- sizeof的返回值为size_t
- size_t类型在32位操作系统下是unsigned int,是一个无符号的整数
2.8 类型限定符
限定符 | 含义 |
---|---|
extern | 声明一个变量,extern声明的变量没有建立存储空间。 extern int a;//变量在定义的时候创建存储空间 |
const | 定义一个常量,常量的值不能修改。 const int a = 10; |
volatile | 防止编译器优化代码 |
register | 定义寄存器变量,提高效率。register是建议型的指令,而不是命令型的指令,如果CPU有空闲寄存器,那么register就生效,如果没有空闲寄存器,那么register无效。 |
2.9字符串格式化输入输出
2.9.1 字符串常量
-
字符串是内存中一段连续的char空间,以'\0'(数字0)结尾
-
字符串常量是由双引号括起来的字符序列,如“china”、“C program”,“$12.5”等都是合法的字符串常量
-
字符串常量与字符常量的不同:
每个字符串的结尾,编译器会自动的添加一个结束标志位'\0',即 "a" 包含两个字符'a'和’\0’
2.9.2 printf函数和putchar函数
-
printf()是C语言标准库函数,用于将格式化后的字符串输出到标准输出。标准输出,即标准输出文件,对应终端的屏幕。printf()申明于头文件stdio.h。
-
函数原型:
int printf ( const char * format, ... );
-
返回值:
正确返回输出的字符总数,错误返回负值,与此同时,输入输出流错误标志将被置值,可由指示器ferror来检查输入输出流的错误标志。
-
调用格式:printf()函数的调用格式为:
printf("格式化字符串",输出表列)
格式化字符串包含三种对象,分别为:
- 字符串常量
- 格式控制字符串
- 转义字符
字符串常量原样输出,在显示中起提示作用。输出表列中给出了各个输出项,要求格式控制字符串和各输出项在数量和类型上应该一一对应。其中格式控制字符串是以%开头的字符串,在%后面跟有各种格式控制符,以说明输出数据的类型、宽度、精度等
-
-
putchar函数是字符输出函数,其功能是在终端(显示器)输出单个字符。其函数原型为
int putchar(int ch);
ch表示要输出的字符内容,返回值作用为:如果输出成功返回一个字符的ASCII码,失败则返回EOF即-1
-
printf格式字符:
打印格式 对应数据类型 含义 %d int 接受整数值并将它表示为有符号的十进制整数 %hd short int 短整数 %hu unsigned short 无符号短整数 %o unsigned int 无符号8进制整数 %u unsigned int 无符号10进制整数 %x,%X unsigned int 无符号16进制整数,x对应的是abcdef,X对应的是ABCDEF %f float 单精度浮点数 %lf double 双精度浮点数 %e,%E double 科学计数法表示的数,此处"e"的大小写代表在输出时用的"e"的大小写 %c char 字符型。可以把输入的数字按照ASCII码相应转换为对应的字符 %s char * 字符串。输出字符串中的字符直至字符串中的空字符(字符串以'\0‘结尾,这个'\0'即空字符)即碰到‘\0’就会停止输出 %p void * 以16进制形式输出指针 %% % 输出一个百分号 printf附加格式:
字符 含义 l(字母l) 附加在d,u,x,o前面,表示长整数 - 左对齐 如%-5d m(代表一个整数) 数据最小宽度 如%5d 0(数字0) 将输出的前面补上0直到占满指定列宽为止不可以搭配使用 - m.n(代表一个整数) m指域宽,即对应的输出项在输出设备上所占的字符数。n指精度,用于说明输出的实型数的小数位数。对数值型的来说,未指定n时,隐含的精度为n=6位。 -
示例:
#include <stdio.h> int main() { int a = 100; printf("a = %d\n", a);//格式化输出一个字符串 printf("%p\n", &a);//输出变量a在内存中的地址编号 printf("%%d\n"); char c = 'a'; putchar(c);//putchar只有一个参数,就是要输出的char long a2 = 100; printf("%ld, %lx, %lo\n", a2, a2, a2); long long a3 = 1000; printf("%lld, %llx, %llo\n", a3, a3, a3); int abc = 10; printf("abc = '%6d'\n", abc); printf("abc = '%-6d'\n", abc); printf("abc = '%06d'\n", abc); printf("abc = '%-06d'\n", abc); double d = 12.3; printf("d = \' %-10.3lf \'\n", d); return 0; }
输出结果:
2.9.3 scanf函数
scanf函数介绍
-
scanf()是C语言中的一个输入函数。与printf函数一样,都被声明在头文件stdio.h里,因此在使用scanf函数时要加上#include <stdio.h>。它是格式输入函数,即按用户指定的格式从键盘上把数据输入到指定的变量之中
-
函数原型
int scanf(const char * format,...);
-
参数:函数的第一个参数是格式字符串,它指定了输入的格式,并按照格式说明符解析输入对应位置的信息并存储于可变参数列表中对应的指针所指位置。每一个指针要求非空,并且与字符串中的格式符一一顺次对应
-
返回值:scanf函数返回成功读入的数据项数,读入数据时遇到了“文件结束”则返回EOF
-
实例说明:scanf
(
"%d %d",&a,&b);- 函数返回值为int型。如果a和b都被成功读入,那么scanf的返回值就是2
- 如果只有a被成功读入,返回值为1
- 如果a和b都未被成功读入,返回值为0
- 如果遇到错误或遇到end of file,返回值为EOF。end of file相当于Ctrl+z 或者Ctrl+d
-
-
格式说明符
格式说明符 作用 c 读入单个字符(后面不会加上空字节 s 读入一个的字符序列,后面会加上空字节,遇到空白字符(\t \r \n 空格等)完成读取。 d 读入可选有符号十进制整数 p 读入一个指针值 -
第一个参数格式字符串中的空白字符和非空白字符
- 空白字符会使scanf函数在读操作中略去输入中的一个或多个空白字符。空白符可以是空格、制表符也即\t和换行符;本质上,控制串中的空白符使 scanf() 在输入流中读,但不保存结果,直到发现非空白字符为止。
- 一个非空白字符会使scanf()函数在读入时剔除掉与这个非空白字符相同的字符;非空白符使 scanf() 在流中读一个匹配的字符并忽略之。例如,"%d,%d" 使 scanf() 先读入一个整数,读入中放弃逗号,然后读另一个整数 如 12,13
注意:
-
scanf() 中用于保存读入值的变元必须都是变量指针,即相应变量的地址
-
在输入流中,数据项必须由空格、制表符和换行符分割。逗号和分号等不是分隔符,比如以下代码
scanf("%d%d",&r,&c);//将接受输入 10 20,但遇到 10,20 则失败
-
格式命令可以说明最大域宽。 在百分号(%)与格式码之间的整数用于限制从对应域读入的最大字符数
scanf("%20s",address);//可以向 address 读入不多于 20 个字符 //如果输入流的内容多于 20 个字符,则下次 scanf() 从此次停止处开始读入。 若达到最大域宽前已遇到空白符,则对该域的读立即停止;此时,scanf() 跳到下一个域。
-
虽然空格、制表符和新行符都用做域分割符号,但读单字符操作中却按一般字符处理
scanf("%c%c%c",&a,&b,&c);//对输入流 "x y" 调用,返回后,x 在变量 a 中,空格在变量 b 中,y 在变量 c 中
-
在高版本的 Visual Studio 编译器中,scanf 被认为是不安全的,被弃用,应当使用scanf_s代替 scanf
scanf函数使用常见问题
-
如何让scanf()函数正确接受有空格的字符串?
#include <stdio.h> int main() { char a[20]; scanf("%s", a); printf("%s\n", a); return 0; }
-
运行结果:
-
结果分析:
上述程序并没有hello world。因为scanf扫描到"o"后面的空格就认为对str的扫描结束(空格没有被扫描),并忽略后面的"world"。残留的信息 "world"是存在于stdin流中,而不是在键盘中。
-
解决方案:
#include <stdio.h> int main() { char a[20]; scanf("%[^\n]", a); printf("%s\n", a); return 0; }
-
-
键盘缓冲区残余信息问题
#include <stdio.h> int main() { int a; char c; while (1) { scanf("%d", &a); scanf("%c", &c); printf("a=%d c=%c\n", a, c); } return 0; }
-
运行结果:
-
结果分析:
输入1后输入"Enter"键,即向键盘缓冲区发去一个“换行"(\n),在这里\n被scanf()函数处理掉了,“错误”地赋给了c.
-
解决方案:
可以在两个scanf()函数之后加getchar()
-
2.9.4 getchar函数
getchar函数介绍
-
函数原型
int getchar(void);// 返回类型为int,参数为void
-
头文件:#include<stdio.h>
-
返回值:
- getchar返回的是字符的ASCII码值(整型)
- getchar在读取结束或者失败的时候,会返回EOF;(EOF意思是end of file,本质上是-1)
-
读取方式:只能输入字符型,输入时遇到回车键才从缓冲区依次提取字符。
-
结束输入的方式:以Enter结束输入(空格不结束),接受空格符
-
舍弃回车符的方法:以Enter结束输入时,接受空格,会舍弃最后的回车符
getchar函数执行过程详解
-
程序执行到getchar()函数时,自动从输入缓冲区中去找字符,如果输入缓冲区中没有字符的话,那么就等待用户输入字符,此时用户使用键盘输入的字符,被输入到输入缓冲区中,键盘输入字符的时候首先进入输入缓冲区,然后getchar()函数获得的字符是从输入缓冲区中提取的且每次只能提取一个字符。
-
用法示例
#include<stdio.h> int main() { int ch = getchar();//输入字符,最好用整型接收 putchar(ch); return 0; }
-
结果解析:
它的简单意思就是从键盘读入一个字符,然后输出到屏幕。理所当然,我们输入A,输出就是A,输入B,输出就是B。
那么我们如果输出的是ABC呢?答案是A。
解释如下:当我们从键盘输入字符‘A’,‘B’, 'C',并按下回车后,我们的输入被放入了输入缓冲区,这个时候getchar()会从缓冲区中读取我们刚才的输入,一次只读一个字符,所以字符A就被拿出来了,赋值给了ch,然后putchar()又将ch放在了标准输出,也就是这里的屏幕,所以我们看见了最终的显示结果A。同时字符‘A’也被缓冲区释放了,而字符‘B’,'C'仍然被留在了缓冲区。而这样是很不安全的,有可能下次使用的时候,我们的缓冲区会读到一些垃圾,但是当程序结束的时候,它会自动刷新。
三、运算符
3.1 三元运算符
-
基本语法
条件表达式 ? 表达式1: 表达式2
- 如果条件表达式为非0 (真),运算后的结果是表达式1;
- 如果条件表达式为0 (假),运算后的结果是表达式2;
-
使用细节
- 表 达式1和表达式2要为可以赋给接收变量的类型( 或可以自动转换), 否则会有精度损失 如:int n=a>b?1.1:1.2 //警告 double->int
- 三元运算符可以转成if--else 语句
3.2 运算符优先级
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|---|---|---|---|---|
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | 无 |
() | 圆括号 | (表达式)/函数名(形参表) | |||
. | 成员选择(对象) | 对象.成员名 | |||
-> | 成员选择(指针) | 对象指针->成员名 | |||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
~ | 按位取反运算符 | ~表达式 | |||
++ | 自增运算符 | ++变量名/变量名++ | |||
-- | 自减运算符 | --变量名/变量名-- | |||
* | 取值运算符 | *指针变量 | |||
& | 取地址运算符 | &变量名 | |||
! | 逻辑非运算符 | !表达式 | |||
(类型) | 强制类型转换 | (数据类型)表达式 | 无 | ||
sizeof | 长度运算符 | sizeof(表达式) | |||
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | |||
% | 余数(取模) | 整型表达式%整型表达式 | |||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | |||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | |||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | |||
< | 小于 | 表达式<表达式 | |||
<= | 小于等于 | 表达式<=表达式 | |||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | |||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | 无 |
/= | 除后赋值 | 变量/=表达式 | |||
*= | 乘后赋值 | 变量*=表达式 | |||
%= | 取模后赋值 | 变量%=表达式 | |||
+= | 加后赋值 | 变量+=表达式 | |||
-= | 减后赋值 | 变量-=表达式 | |||
<<= | 左移后赋值 | 变量<<=表达式 | |||
>>= | 右移后赋值 | 变量>>=表达式 | |||
&= | 按位与后赋值 | 变量&=表达式 | |||
^= | 按位异或后赋值 | 变量^=表达式 | |||
|= | 按位或后赋值 | 变量|=表达式 | |||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | 无 |
四、程序流程控制
4.1 switch分支结构
4.1.1 基本语法
switch(表达式){
case 常量 1 : // 当表达式值等于 常量 1
语句块 1 ;
break; // 退出 switch
case 常量 2 ; // 含义一样
语句块 2 ;
break ;
...
case 常量 n;
语句块 n ;
break ;
default :
default 语句块 ;
break ;
}
示例:
#include <stdio.h>
int main()
{
char c;
c = getchar();
switch (c) //参数只能是整型变量
{
case '1':
printf("OK\n");
break;//switch遇到break就中断了
case '2':
printf("not OK\n");
break;
default://如果上面的条件都不满足,那么执行default
printf("are u ok?\n");
}
return 0;
}
4.1.2 switch 细节讨论
- switch 语句中的 expression 是一个常量表达式,必须是一个整型(char、short、int、long等) 或枚举类型
- case子句中的值必须是常量,而不能是变量
- default子句是可选的,当没有匹配的case时,执行default
- break语句用来在执行完一个case分支后使程序跳出switch语句块;
- 如果没有写break,会执行下一个case 语句块,直到遇到break 或者执行到switch结尾, 这个现象称为穿透.
4.2 do...while循环控制
4.2.1基本语法
①循环变量初始化;
do{
②循环体(多条语句);
③循环变量迭代;
}while(④循环条件);
注意:do – while 后面有一个 分号,不能省略
- 执行流程图:
-
用法示例:
#include <stdio.h> int main() { int a = 1; do { a++; printf("a = %d\n", a); } while (a < 10); return 0; }
-
注意事项
do..while循环是先执行,再判断
4.3 跳转控制语句
4.3.1 break语句
- 在switch条件语句和循环语句中都可以使用break语句:
- 当它出现在switch条件语句中时,作用是终止某个case并跳出switch结构
- 当它出现在循环语句中,作用是跳出当前内循环语句,执行后面的代码
- 当它出现在嵌套循环语句中,跳出最近的内循环语句,执行后面的代码
4.3.2 continue语句
-
在循环语句中,如果希望立即终止本次循环,并执行下一次循环,此时就需要使用continue语句
-
以do-while使用continue为例,画出示意图
-
注意事项
continue语句,只能配合循环语言使用,不能单独和switch/if使用
4.3.3 goto语句
-
C 语言的 goto 语句可以无条件地转移到程序中指定的行
-
goto语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能
-
在C程序设计中 一般不主张使用goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难
-
用法示例
int main() { printf("start\n"); goto lable1; //lable1 称为标签 printf("ok1\n"); printf("ok2\n"); lable1: printf("ok3\n"); printf("ok4\n"); } //输出 ok3 和 ok4
五、数组和字符串
5.1 数组概述
-
在程序设计中,为了方便处理数据把具有相同类型的若干变量按有序形式组织起来——称为数组
-
数组就是在内存中连续的相同类型的变量空间。同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的。
注意:数组名就代表该数组的首地址 ,即 a[0]地址
-
数组属于构造数据类型
-
一个数组可以分解为多个数组元素:这些数组元素可以是基本数据类型或构造类型
int a[10]; struct Stu boy[10];
-
按数组元素类型的不同,数组可分为:数值数组、字符数组、指针数组、结构数组等类别
int a[10]; char s[10]; char *p[10];
-
-
通常情况下,数组元素下标的个数也称为维数,根据维数的不同,可将数组分为一维数组、二维数组、三维数组、四维数组等。通常情况下,我们将二维及以上的数组称为多维数组
5.2 一维数组
5.2.1 一维数组的定义
-
数组名字符合标识符的书写规定(数字、英文字母、下划线)
-
数组名不能与其它变量名相同,同一作用域内是唯一的
-
方括号[]中常量表达式表示数组元素的个数;int a[3]表示数组a有3个元素。其下标从0开始计算,因此3个元素分别为a[0],a[1],a[2]
-
定义数组时[]内最好是常量,使用数组时[]内即可是常量,也可以是变量
-
数组的定义
数据类型 数组名 [数组大小]; int a [5]; // a 数组名,类型 int , [5] 大小, 即 a 数组最多存放 5个int 数据 //赋初值 a[0] = 1; a[1] = 30; ....
5.2.2 数组的使用
-
访问数组元素
数组名[下标] 比如:你要使用 a 数组的第三个元素 a[2 ], 下标是从 0 开始计算
-
一维数组的初始化
在定义数组的同时进行赋值,称为初始化。全局数组若不初始化,编译器将其初始化为零。局部数组若不初始化,内容为随机值
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量 int a[10] = { 1, 2, 3 };//初始化前三个成员,后面所有元素都设置为0 int a[10] = { 0 };//所有的成员都设置为0 //[]中不定义元素个数,定义时必须初始化 int a[] = { 1, 2, 3, 4, 5 };//定义了一个数组,有5个成员
-
数组名
数组名是一个地址的常量,代表数组中首元素的地址
#include <stdio.h> int main() { int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量 printf("a = %p\n", a);//a = 000000000062FDE0 printf("&a[0] = %p\n", &a[0]);//&a[0] = 000000000062FDE0 int n = sizeof(a); //数组占用内存的大小,10个int类型,10 * 4 = 40 int n0 = sizeof(a[0]);//数组第0个元素占用内存大小,第0个元素为int,4 int i = 0; for (i = 0; i < sizeof(a) / sizeof(a[0]); i++) { printf("%d ", a[i]); } printf("\n"); return 0; }
注意:C的数组属构造类型, 是引用传递(传递的是地址),因此当把一个数组传递给一个函数时/或者变量,函数/变量操作数组会影响到原数组
5.3 二维数组
5.3.1 二维数组的定义
-
二维数组定义的一般形式是:
类型说明符 数组名[常量表达式1][常量表达式2] //其中常量表达式1 表示第一维下标的长度,常量表达式2 表示第二维下标的长度。如:int a[3][4];
-
命名规则同一维数组
-
定义了一个三行四列的数组,数组名为a其元素类型为整型,该数组的元素个数为3×4个,即 int a [3][4];
注:二维数组a是按行进行存放的,先存放a[0]行,再存放a[1]行、a[2]行,并且每行有四个元素,也是依次存放的
- 二维数组在概念上是二维的:其下标在两个方向上变化,对其访问一般需要两个下标
- 在内存中并不存在二维数组,二维数组实际的硬件存储器是连续编址的,也就是说内存中只有一维数组,即放完一行之后顺次放入第二行,和一维数组存放方式是一样的
-
用法示例:
#include <stdio.h> int main() { //定义了一个二维数组,名字叫a //由3个一维数组组成,这个一维数组是int [4] //这3个一维数组的数组名分别为a[0],a[1],a[2] int a[3][4]; a[0][0] = 0; //…… a[2][3] = 12; //给数组每个元素赋值 int i = 0; int j = 0; int num = 0; for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { a[i][j] = num++; } } //遍历数组,并输出每个成员的值 for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { printf("%d, ", a[i][j]); } printf("\n"); } return 0; }
5.3.2 二维数组的初始化
//分段赋值 int a[3][4] = {{ 1, 2, 3, 4 },{ 5, 6, 7, 8, },{ 9, 10, 11, 12 }};
int a[3][4] =
{
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8, },
{ 9, 10, 11, 12 }
};
//连续赋值
int a[3][4] = { 1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12 };
//可以只给部分元素赋初值,未初始化则为0
int a[3][4] = { 1, 2, 3, 4 };
//所有的成员都设置为0
int a[3][4] = {0};
//[]中不定义元素个数,定义时必须初始化
int a[][4] = { 1, 2, 3, 4, 5, 6, 7, 8};
5.3.3 数组名
数组名是一个地址的常量,代表数组中首元素的地址
#include <stdio.h>
int main()
{
//定义了一个二维数组,名字叫a
//二维数组是本质上还是一维数组,此一维数组有3个元素
//每个元素又是一个一维数组int[4]
int a[3][4] = { 1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12 };
//数组名为数组首元素地址,二维数组的第0个元素为一维数组
//第0个一维数组的数组名为a[0]
printf("a = %p\n", a);//a = 000000000062FDF0
printf("a[0] = %p\n", a[0]);//a[0] = 000000000062FDF0
//测二维数组所占内存空间,有3个一维数组,每个一维数组的空间为4*4
//sizeof(a) = 3 * 4 * 4 = 48
printf("sizeof(a) = %d\n", sizeof(a));
//测第0个元素所占内存空间,a[0]为第0个一维数组int[4]的数组名,4*4=16
printf("sizeof(a[0]) = %d\n", sizeof(a[0]) );
//测第0行0列元素所占内存空间,第0行0列元素为一个int类型,4字节
printf("sizeof(a[0][0]) = %d\n", sizeof(a[0][0]));
//求二维数组行数
printf("i = %d\n", sizeof(a) / sizeof(a[0]));
// 求二维数组列数
printf("j = %d\n", sizeof(a[0]) / sizeof(a[0][0]));
//求二维数组行*列总数
printf("n = %d\n", sizeof(a) / sizeof(a[0][0]));
return 0;
}
5.4 字符数组和字符串
5.4.1 字符数组与字符串区别
-
C语言中没有字符串这种数据类型,可以通过char的数组来替代
-
字符串一定是一个char的数组,但char的数组未必是字符串
-
数字0(和字符‘\0’等价)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组
-
示例:
#include <stdio.h> int main() { char c1[] = { 'c', ' ', 'p', 'r', 'o', 'g' }; //普通字符数组 printf("c1 = %s\n", c1); //乱码,因为没有’\0’结束符 char c1[7] = { 'c', ' ', 'p', 'r', 'o', 'g' }; //字符串 printf("c1 = %s\n", c1); //正常输出,因为字符数组的大小为7,最后一个元素没有初始化,置为0 //以‘\0’(‘\0’就是数字0)结尾的字符数组是字符串 char c2[] = { 'c', ' ', 'p', 'r', 'o', 'g', '\0'}; printf("c2 = %s\n", c2); //字符串处理以‘\0’(数字0)作为结束符,后面的'h', 'l', 'l', 'e', 'o'不会输出 char c3[] = { 'c', ' ', 'p', 'r', 'o', 'g', '\0', 'h', 'l', 'l', 'e', 'o', '\0'}; printf("c3 = %s\n", c3); return 0; }
5.4.2 字符串的初始化
#include <stdio.h>
// C语言没有字符串类型,通过字符数组模拟
// C语言字符串,以字符‘\0’, 数字0
int main()
{
//不指定长度, 没有0结束符,有多少个元素就有多长
char buf[] = { 'a', 'b', 'c' };
printf("buf = %s\n", buf); //乱码
//指定长度,后面没有赋值的元素,自动补0
char buf2[100] = { 'a', 'b', 'c' };
char buf1[1000] = { "hello"};
printf("buf2 = %s\n", buf2);
//所有元素赋值为0
char buf3[100] = { 0 };
//char buf4[2] = { '1', '2', '3' };//数组越界
char buf5[50] = { '1', 'a', 'b', '0', '7' };
printf("buf5 = %s\n", buf5);
char buf6[50] = { '1', 'a', 'b', 0, '7' };
printf("buf6 = %s\n", buf6);
char buf7[50] = { '1', 'a', 'b', '\0', '7' };
printf("buf7 = %s\n", buf7);
//使用字符串初始化,编译器自动在后面补0,常用
char buf8[] = "hello";//实际上该字符数组的大小为6,等价于buf8[] = {'h','e','l','l','o','\0'}
//'\0'后面最好不要连着数字,有可能几个数字连起来刚好是一个转义字符
//'\ddd'八进制字义字符,'\xdd'十六进制转移字符
// \012相当于\n
char str[] = "\012abc";
printf("str == %s\n", str);
return 0;
}
5.4.3 字符串的输入输出
由于字符串采用了'\0'标志,字符串的输入输出将变得简单方便。
#include <stdio.h>
int main()
{
char str[100];
printf("input string1 : \n");
scanf("%s", str);//scanf(“%s”,str)默认以空格分隔
printf("output:%s\n", str);
return 0;
}
5.4.3.1 gets()
-
函数原型:char *gets(char *str)
-
功能:从标准输入读入字符,并保存到str指定的内存空间,直到出现换行符或读到文件结尾为止
-
参数:str:字符串首地址
-
返回值:
- 成功:读入的字符串
- 失败:NULL
-
gets(str)与scanf(“%s”,str)的区别:
- gets(str)允许输入的字符串含有空格
- scanf(“%s”,str)不允许含有空格
-
关于使用 gets() 函数需要注意:使用 gets() 时,系统会将最后“敲”的换行符从缓冲区中取出来,然后丢弃,所以缓冲区中不会遗留换行符。这就意味着,如果前面使用过 gets(),而后面又要从键盘给字符变量赋值的话就不需要吸收回车清空缓冲区了,因为缓冲区的回车已经被 gets() 取出来扔掉了
验证:
# include <stdio.h> int main(void) { char str[30]; char ch; printf("请输入字符串:"); gets(str); printf("%s\n", str); scanf("%c", &ch); printf("ch = %c\n", ch); return 0; }
注意:由于scanf()和gets()无法知道字符串s大小,必须遇到换行符或读到文件结尾为止才接收输入,因此容易导致字符数组越界(缓冲区溢出)的情况
5.4.3.2 fgets()
-
函数原型:char *fgets(char *s,int size,FILE *stream);
-
功能:从stream指定的文件内读入字符,保存到s所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
-
参数:
- s:字符串
- size:指定最大读取字符串的长度(size - 1)
- stream:文件指针,如果读键盘输入的字符串,固定写为stdin
-
返回值:
- 成功:成功读取的字符串
- 读到文件尾或出错: NULL
-
示例:
int main(void) { char ch[10]; //"hello\n\0" //fgets可以接收空格 //fgets获取字符串少于元素个数会有\n 大于等于 没有\n fgets(ch, sizeof(ch), stdin); printf("%s", ch); return 0; }
注意:fgets()在读取一个用户通过键盘输入的字符串的时候,同时把用户输入的回车也做为字符串的一部分。通过scanf和gets输入一个字符串的时候,不包含结尾的“\n”,但通过fgets结尾多了“\n”。fgets()函数是安全的,不存在缓冲区溢出的问题
5.4.3.3 puts()
-
函数原型:int puts(const char *s);
-
功能:标准设备输出s字符串,遇到'\0'停止,在输出完成后自动输出一个'\n'
-
参数:s:字符串首地址
-
返回值:
- 成功:非负数
- 失败:-1
-
示例:
int main(void) { char ch[] = "hello world"; //puts自带换行 //puts(ch); //puts("hello\0 world"); //puts("");换行 return 0; }
5.4.3.4 fputs()
-
函数原型:int fputs(const char * str, FILE * stream);
-
功能:将str所指定的字符串写入到stream指定的文件中, 字符串结束符 '\0' 不写入文件
-
参数:
- str:字符串
- stream:文件指针,如果把字符串输出到屏幕,固定写为stdout
-
返回值:
- 成功:0
- 失败:-1
-
示例:
int main(void) { char ch[] = "hello world"; //fputs(ch, stdout); //printf("%s", ch); return 0; }
注意:fputs()是puts()的文件操作版本,但fputs()不会自动输出一个'\n'
六、函数
6.1 概述
6.1.1 函数分类
C 程序是由函数组成的,我们写的代码都是由主函数 main()开始执行的。函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。
从函数定义的角度看,函数可分为系统函数和用户定义函数两种:
- 系统函数,即库函数:这是由编译系统提供的,用户不必自己定义这些函数,可以直接使用它们,如我们常用的打印函数printf()。
- 用户定义函数:用以解决用户的专门需要。
6.1.2 函数的作用
- 函数的使用可以省去重复代码的编写,降低代码重复率
- 函数可以让程序更加模块化,从而有利于程序的阅读,修改和完善
6.1.3 函数的调用
当调用函数时,需要关心5要素:
-
头文件:包含指定的头文件
-
函数名字:函数名字必须和头文件声明的名字一样
-
功能:需要知道此函数能干嘛后才调用
-
参数:参数类型要匹配
-
返回值:根据需要接收返回值
-
示例:
#include <time.h> time_t time(time_t *t); 功能:获取当前系统时间 参数:常设置为NULL 返回值:当前系统时间, time_t 相当于long类型,单位为毫秒 #include <stdlib.h> void srand(unsigned int seed); 功能:用来设置rand()产生随机数时的随机种子 参数:如果每次seed相等,rand()产生随机数相等 返回值:无 #include <stdlib.h> int rand(void); 功能:返回一个随机数值 参数:无 返回值:随机数 #include <stdio.h> #include <time.h> #include <stdlib.h> int main() { time_t tm = time(NULL);//得到系统时间 srand((unsigned int)tm);//随机种子只需要设置一次即可 int r = rand(); printf("r = %d\n", r); return 0; }
6.2 函数的定义
6.2.1 函数定义的格式
函数定义的一般形式:
返回类型 函数名(形式参数列表)
{
数据定义部分;
执行语句部分;
}
6.2.2 函数名字、形参、函数体、返回值
-
函数名
理论上是可以随意起名字,最好起的名字见名知意,应该让用户看到这个函数名字就知道这个函数的功能。注意,函数名的后面有个圆换号(),代表这个为函数,不是普通的变量名
-
形参列表
在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以,形参里的变量不能赋值
//在定义函数时指定的形参,必须是,类型+变量的形式: //1: right, 类型+变量 void max(int a, int b) { } //2: error, 只有类型,没有变量 void max(int, int) { } //3: error, 只有变量,没有类型 int a, int b; void max(a, b) { } //4: error, 形参不能赋值 void max(int a = 10, int b = 20) //在定义函数时指定的形参,可有可无,根据函数的需要来设计,如果没有形参,圆括号内容为空,或写一个void关键字: // 没形参, 圆括号内容为空 void max() { } // 没形参, 圆括号内容为void关键字 void max(void) { }
-
函数体
花括号{ }里的内容即为函数体的内容,这里为函数功能实现的过程,这和以前的写代码没太大区别,以前我们把代码写在main()函数里,现在只是把这些写到别的函数里。
-
返回值
函数的返回值是通过函数中的return语句获得的,return后面的值也可以是一个表达式。
//a)尽量保证return语句中表达式的值和函数返回类型是同一类型。 int max() // 函数的返回值为int类型 { int a = 10; return a;// 返回值a为int类型,函数返回类型也是int,匹配 } //b)如果函数返回的类型和return语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以自动进行类型转换。 double max() // 函数的返回值为double类型 { int a = 10; return a;// 返回值a为int类型,它会转为double类型再返回 } //c)return语句的另一个作用为中断return所在的执行函数,类似于break中断循环、switch语句一样。 int max() { return 1;// 执行到,函数已经被中断,所以下面的return 2无法被执行到 return 2;// 没有执行 } //d)如果函数带返回值,return后面必须跟着一个值,如果函数没有返回值,函数名字的前面必须写一个void关键字,这时候,我们写代码时也可以通过return中断函数(也可以不用),只是这时,return后面不带内容( 分号“;”除外)。 void max()// 最好要有void关键字 { return; // 中断函数,这个可有可无 }
注意:如果函数返回的类型和return语句中表达式的值不一致,而它又无法自动进行类型转换,程序则会报错。
6.3 函数的调用
定义函数后,我们需要调用此函数才能执行到这个函数里的代码段。这和main()函数不一样,main()为编译器设定好自动调用的主函数,无需人为调用,我们都是在main()函数里调用别的函数,一个 C 程序里有且只有一个main()函数
6.3.1 函数执行流程
#include <stdio.h>
void print_test()
{
printf("this is for test\n");
}
int main()
{
print_test(); // print_test函数的调用
return 0;
}
- 进入main()函数
- 调用print_test()函数:
- 它会在main()函数的前寻找有没有一个名字叫“print_test”的函数定义;
- 如果找到,接着检查函数的参数,这里调用函数时没有传参,函数定义也没有形参,参数类型匹配;
- 开始执行print_test()函数,这时候,main()函数里面的执行会阻塞( 停 )在print_test()这一行代码,等待print_test()函数的执行。
- print_test()函数执行完( 这里打印一句话 ),main()才会继续往下执行,执行到return 0, 程序执行完毕。
6.3.2 函数的形参和实参
- 形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。
- 实参出现在主调函数中,进入被调函数后,实参也不能使用。
- 实参变量对形参变量的数据传递是“值传递”,即单向传递,只由实参传给形参,而不能由形参传回来给实参。
- 在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放。
- 实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主调函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
6.3.3 函数参数的传递方式
C语言传递参数可以是 值传递(pass by value),也可以是 传递指针(a pointer passed by value)也叫传递地址或者引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低
6.3.4 无参函数调用
如果是调用无参函数,则不能加上“实参”,但括号不能省略。
/ 函数的定义
void test()
{
}
int main()
{
// 函数的调用
test(); // right, 圆括号()不能省略
test(250); // error, 函数定义时没有参数
return 0;
}
6.3.5 有参函数调用
//a)如果实参表列包含多个实参,则各参数间用逗号隔开
// 函数的定义
void test(int a, int b)
{
}
int main()
{
int p = 10, q = 20;
test(p, q); // 函数的调用
return 0;
}
//b)实参与形参的个数应相等,类型应匹配(相同或赋值兼容)。实参与形参按顺序对应,一对一地传递数据。
//c)实参可以是常量、变量或表达式,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。所以,这里的变量是在圆括号( )外面定义好、赋好值的变量。
// 函数的定义
void test(int a, int b)
{
}
int main()
{
// 函数的调用
int p = 10, q = 20;
test(p, q); // right
test(11, 30 - 10); // right
test(int a, int b); // error, 不应该在圆括号里定义变量
return 0;
}
6.3.6 函数返回值
//a)如果函数定义没有返回值,函数调用时不能写void关键字,调用函数时也不能接收函数的返回值。
// 函数的定义
void test()
{
}
int main()
{
// 函数的调用
test(); // right
void test(); // error, void关键字只能出现在定义,不可能出现在调用的地方
int a = test(); // error, 函数定义根本就没有返回值
return 0;
}
//b)如果函数定义有返回值,这个返回值我们根据用户需要可用可不用,但是,假如我们需要使用这个函数返回值,我们需要定义一个匹配类型的变量来接收
// 函数的定义, 返回值为int类型
int test()
{
}
int main()
{
// 函数的调用
int a = test(); // right, a为int类型
int b;
b = test(); // right, 和上面等级
char *p = test(); // 虽然调用成功没有意义, p为char *, 函数返回值为int, 类型不匹配
// error, 必须定义一个匹配类型的变量来接收返回值
// int只是类型,没有定义变量
int = test();
// error, 必须定义一个匹配类型的变量来接收返回值
// int只是类型,没有定义变量
int test();
return 0;
}
6.4 函数的声明
如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。
所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译能正常进行。
注意:一个函数只能被定义一次,但可以声明多次。
#include <stdio.h>
int max(int x, int y); // 函数的声明,分号不能省略
// int max(int, int); // 另一种方式
int main()
{
int a = 10, b = 25, num_max = 0;
num_max = max(a, b); // 函数的调用
printf("num_max = %d\n", num_max);
return 0;
}
// 函数的定义
int max(int x, int y)
{
return x > y ? x : y;
}
函数定义和声明的区别:
-
定义是指对函数功能的确立,包括指定函数名、函数类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。
-
声明的作用则是把函数的名字、函数类型以及形参的个数、类型和顺序(注意,不包括函数体)通知编译系统,以便在对包含函数调用的语句进行编译时,据此对其进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。
七、预处理命令
7.1 预处理命令基本介绍
- 使用库函数之前,应该用#include引入对应的头文件。这种以# 号开头的命令称为预处理命令
- 这些在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)
- 预处理主要是处理以#开头的命令,例如#include <stdio.h>等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面
- 预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译
- C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计
7.2 头文件
7.2.1 引入
在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如hello.c中,去使用myfuns.c 文件中的函数,如何实现? ——头文件
7.2.2 头文件基本概念
- 头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和C标准库自带的头文件
- 在程序中要使用头文件,需要使用C预处理指令 #include 来引用它。前面我们已经看过 stdio.h 头文件,它是C标准库自带的头文件
- #include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种
- #include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候
- 建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件
7.2.3 头文件的示意图
7.2.4 头文件快速入门
-
说明:头文件快速入门——C 程序相互调用函数,我们将 cal 声明到文件 myfun.h , 在 myfun.c 中定义 cal 函数,当其它文件需要使用到 myfun.h 声明 的函数时,可以#include 该头文件,就可以使用了
-
示例:
myfun.h #include <stdio.h> //声明函数 int myCal(int n1, int n2, char oper); void sayHello() ;
myfun.c #include <stdio.h> //实现 int myCal(int n1, int n2, char oper) int myCal(int n1, int n2, char oper) { //定义一个变量 res ,保存运算的结果 double res = 0.0; switch(oper) { case '+' : res = n1 + n2; break; case '-': res = n1 - n2; break; case '*': res = n1 * n2; break; case '/': res = n1 / n2; break; default : printf("你的运算符有误~"); } printf("\n%d %c %d = %.2f\n", n1, oper, n2, res); return res; } void sayHello() { //定义函数 printf("say Hello"); }
hello.c #include <stdio.h> //引入我们需要的头文件(用户头文件) #include "myfun.h" void main() { //使用 myCal 完成计算任务 int n1 = 10; int n2 = 50; char oper = '-'; double res = 0.0; //调用 myfun.c 中定义的函数 myCal res = myCal(n1, n2, oper); printf("\nres=%.2f", res); sayHello(); getchar(); }
7.2.5 头文件的注意事项和细节说明
- 引用头文件相当于复制头文件的内容
- 源文件的名字 可以不和头文件一样,但是为了好管理,一般头文件名和源文件名一样
- C 语言中 include <> 与 include "" 的区别
- include <>:引用的是编译器的类库路径里面的头文件,用于引用系统头文件
- include "":引用的是你程序目录的相对路径中的头文件,如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找该头文件,用于引用用户头文件。
- 所以:
- 引用 系统头文件,两种形式都会可以,include <> 效率高
- 引用 用户头文件,只能使用 include ""
- 一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令
- 同一个头文件如果被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制
- 在一个被包含的文件(.c)中又可以包含另一个文件头文件
- 不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误(!!!!)
7.3 C语言宏定义
基本介绍:
-
#define 叫做宏定义命令,它也是 C 语言预处理命令的一种 。 所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串
-
示例:
#define N 100 int main(){ int sum = 20 + N;//int sum = 20 +100 printf("%d\n", sum); getchar(); return 0; } //说明 小结: int sum = 20 + N,N 被 100 代替了。 #define N 100 就是宏定义,N 为宏名,100 是宏的内容(宏所表示的字符串)。在预处理阶段,对程序中所有出现的“宏名”,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。 宏定义是由源程序中的宏定义命令#define 完成的,宏替换是由预处理程序完成的
7.4 宏定义的形式
-
语法:#define 宏名 字符串
- #表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等
- 这里所说的字符串是一般意义上的字符序列,不要和 C 语言中的字符串等同,它不需要双引号
- 程序中反复使用的表达式就可以使用宏定义
-
示例:
#include <stdio.h> //宏定义 , 宏名 M , 对应的字符串 (n*n+3*n) // 注意:如果宏对应的字符串 有 ( ) , 那么就不能省略 #define M (n*n+3*n) int main(){ int sum, n; printf("Input a number: "); scanf("%d", &n); //n = 3 sum = 3*M+4*M+5*M; // 宏展开 3*(n*n+3*n)+4*(n*n+3*n)+5*(n*n+3*n) printf("sum=%d\n", sum); getchar(); getchar(); return 0; }
-
宏定义注意事项和细节
-
宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
-
宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换
-
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef 命令
-
示例:
#define PI 3.14159 int main(){ printf("PI=%f", PI); return 0; } #undef PI //取消宏定义 void func(){ // Code printf("PI=%f", PI);//错误,这里不能使用到 PI 了 }
-
代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替
#include <stdio.h> #define OK 100 int main(){ printf("OK\n"); return 0; }
-
宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换
#define PI 3.1415926 #define S PI*y*y /* PI 是已定义的宏名*/ printf("%f", S); //在宏替换后变为: printf("%f", 3.1415926*y*y)
-
习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母
-
可用宏定义表示数据类型,使书写方便
#define UINT unsigned int void main() { UINT a, b; // 宏替换 unsigned int a, b; }
-
宏定义表示数据类型和用 typedef 定义数据说明符的区别: 宏定义只是简单的字符串替换,而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型
-
7.5 带参数的宏定义
-
基本介绍
-
C 语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似
-
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参
-
带参宏定义的一般形式为
#define 宏名(形参列表)
字符串 ,在字符串中可以含有各个形参 -
带参宏调用的一般形式为 : 宏名(实参列表)
-
示例:
#include <stdio.h> //说明 //1. MAX 就是带参数的宏 //2. (a,b) 就是形参 //3. (a>b) ? a : b 是带参数的宏对应字符串,该字符串中可以使用形参 #define MAX(a,b) (a>b) ? a : b int main(){ int x , y, max; printf("input two numbers: "); scanf("%d %d", &x, &y); //说明 //1. MAX(x, y); 调用带参数宏定义 //2. 在宏替换时(预处理,由预处理器), 会进行字符串的替换,同时会使用实参, 去替换形参 //3. 即 MAX(x, y) 宏替换后 (x>y) ? x : y max = MAX(x, y); printf("max=%d\n", max); getchar(); getchar(); return 0; }
-
-
带参宏定义的注意事项和细节
-
带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现
#define MAX(a,b) (a>b)?a:b 如果写成了 #define MAX (a, b) (a>b)?a:b 将被认为是无参宏定义,宏名 MAX 代表字符串(a,b) (a>b)?a:b 而不是 : MAX(a,b) 代表 (a>b) ? a: b 了
-
在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型
-
在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
#include <stdlib.h> #define SQ(y) (y)*(y) // 带参宏定义,字符串内的形参通常要用括号括起来以避免出错 int main(){ int a, sq; printf("input a number: "); scanf("%d", &a); sq = SQ(a+1); // 宏替换 (a+1) * (a+1) //如果定义成SQ(y) y*y 宏替换 a+1 * a+1 printf("sq=%d\n", sq); system("pause"); return 0; }
-
7.6 带参宏定义和函数的区别
-
宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。
-
函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码
-
示例:
//案例说明 :要求 使用函数计算平方值, 使用宏计算平方值, 并总结二者的区别 #include <stdlib.h> int SQ(int y){ return ((y)*(y)); } int main(){ int i=1; while(i<=5){ // 1, 4, 9, 16, 25 printf("%d^2 = %d\n", (i-1), SQ(i++)); } system("pause"); return 0; //------------------------------------------- #include <stdlib.h> #define SQ(y) ((y)*(y)) int main(){ int i=1; while(i<=5){ // 这里相当于计算了 1,3,5 的平方 ////进入循环 3 次,得到的是 1 * 1 = 1 3 * 3 = 9 , 5 * 5 = 25 // SQ(i++) 宏调用 展开 ((i++)*(i++)) printf("%d^2 = %d\n", i, SQ(i++)); } system("pause"); return 0; }
7.7 C 语言预处理命令总结
预处理指令是以#号开头的代码行,# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字,在关键字和 # 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else 条件编译块 |
八、指针
8.1 概述
8.1.1 物理存储器和存储地址空间
有关内存的两个概念:物理存储器和存储地址空间
- 物理存储器:实际存在的具体存储器芯片
- 主板上装插的内存条
- 显示卡上的显示RAM芯片
- 各种适配卡上的RAM芯片和ROM芯片
- 存储地址空间:对存储器编码的范围。我们在软件上常说的内存是指这一层含义。
- 编码:对每个物理存储单元(一个字节)分配一个号码
- 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写
8.1.2 内存地址
- 将内存抽象成一个很大的一维字符数组
- 编码就是对内存的每一个字节分配一个32位或64位的编号(与32位或者64位处理器相关)。这个内存编号我们称之为内存地址
- 内存中的每一个数据都会分配相应的地址:
- char:占一个字节分配一个地址
- int: 占四个字节分配四个地址
- float、struct、函数、数组等
8.1.3 指针和指针变量
- 内存区的每一个字节都有一个编号,这就是“地址”
- 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
- 指针的实质就是内存“地址”。指针就是地址,地址就是指针
- 指针是内存单元的编号,指针变量是存放地址的变量
- 通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样
8.2 指针基础知识
8.2.1 指针变量的定义和使用
-
指针也是一种数据类型,指针变量也是一种变量
-
指针变量指向谁,就把谁的地址赋值给指针变量
-
“*” 操作符操作的是指针变量指向的内存空间
-
示例:
int main() { int a = 0; char b = 100; printf("%p, %p\n", &a, &b); //打印a, b的地址 //int *代表是一种数据类型,int*指针类型,p才是变量名 //定义了一个指针类型的变量,可以指向一个int类型变量的地址 int* p; p = &a;//将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号 //&是取地址符号是升维度的 //*是取值符号是降维度的 printf("%d\n", *p);//p指向了a的地址,*p就是a的值 char* p1 = &b; printf("%c\n", *p1);//*p1指向了b的地址,*p1就是b的值 return 0; }
注意:&可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。
8.2.2 指针大小
-
使用sizeof()测量指针的大小,得到的总是:4或8
-
sizeof()测的是指针变量指向存储地址的大小
-
在32位平台,所有的指针(地址)都是32位(4字节)
-
在64位平台,所有的指针(地址)都是64位(8字节)
int *p1; int **p2; char *p3; char **p4; printf("sizeof(p1) = %d\n", sizeof(p1)); printf("sizeof(p2) = %d\n", sizeof(p2)); printf("sizeof(p3) = %d\n", sizeof(p3)); printf("sizeof(p4) = %d\n", sizeof(p4)); printf("sizeof(double *) = %d\n", sizeof(double *));
8.2.3 野指针和空指针
指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
int main()
{
//不建议将一个变量的值直接赋值给指针
//野指针 -》指针变量指向一个未知的空间
int* p = 100;//程序中允许存在野指针
//操作系统将0-255作为系统占用不允许访问操作
//操作野指针对应的内存空间可能报错
printf("%d\n", *p);
return EXIT_SUCCESS;
}
野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针
int *p = NULL;
NULL是一个值为0的宏常量:#define NULL ((void *)0)
int main(void)
{
//空指针是指内存地址编号为0的空间
int* p = NULL;
//操作空指针对应的空间一定会报错
*p = 100;//这里对空指针进行了赋值操作,出错
printf("%d\n", *p);
//空指针可以用在条件判断
return 0;
}
8.2.4 万能指针void *
void *指针可以指向任意变量的内存空间
int main()
{
int a = 10;
//int* p = &a;
//万能指针可以接收任意类型变量的内存地址
void* p = &a;
//在通过万能指针修改变量的值时 需要找到变量对应的指针类型
*(int*)p = 100;
printf("%d\n", a);
printf("%d\n", *(int*)p);
//printf("%p\n", p);
printf("万能指针在内存占的字节大小:%d\n", sizeof(void*));
//printf("void在内存占的字节大小:%d\n", sizeof(void));无法查看void的大小
return EXIT_SUCCESS;
}
8.2.5 const修饰的指针变量
-
const修饰常量
int main() { //常量 const int a = 10; //a = 100;//err,不可直接修改可通过指针修改 //指针间接修改常量的值 int* p = &a; *p = 100; printf("%d\n", a); return EXIT_SUCCESS; }
-
const修饰指针类型
可以修改指针变量的值,不可以修改指针指向内存空间的值
int main(void) { int a = 10; int b = 20; const int* p = &a; //p = &b;//ok //*p = 100;//err printf("%d\n", *p); return 0; }
-
const修饰指针变量
可以修改指针指向的内存空间的值,不可以修改指针变量的值
int main(void) { int a = 10; int b = 20; int* const p = &a; //p = &b;//err *p = 200;//ok printf("%d\n", a); return 0; }
-
const既修饰指针类型又修饰指针变量
int main(void) { int a = 10; int b = 20; //const修饰指针类型 修饰指针变量 只读指针 const int* const p = &a; //可通过二级指针修改 //二级指针操作 int** pp = &p;//指向p指针的地址 //*pp = &b;改变p指针的值 **pp = 100;//改变p指针指向内容的值为100,即把10改成100 printf("%d\n", *p); //p = &b;//err //*p = 100;//err return 0; }
-
总结:const靠近哪个,哪个便不能修改。但可以通过高一级指针进行修改。即可通过升维来修改
8.3 指针与数组
8.3.1 数组名
- 数组名字是数组的首元素地址,但它是一个常量
int main()
{
int arr[] = {123456,2,3,4,5,6,7,8,9,10};
//数组名是一个常量 不允许赋值
//数组名是数组首元素地址
//arr = 100;//err,即不可给数组名赋值
int* p;
p = arr;
//printf("%p\n", p);
//printf("%p\n", arr);
*p = 123;
for (int i = 0; i < 10; i++)
{
//*==[]??
//printf("%d\n", *(arr+i));//arr[0]
//printf("%d\n", p[i]);
//printf("%d\n", *(p + i));
printf("%d\n", *p);
p++;//指针类型变量+1 等同于内存地址+sizeof(int)
}
//printf("%p\n", arr);
//printf("%p\n", p);
//两个指针相减 等到的结果是两个指针的偏移量 (步长)
//所有的指针类型 相减结果都是int类型
int step = p - arr;//10 +1相当于+sizeof(int) 40/sizeof(int)
printf("%d\n", step);
return EXIT_SUCCESS;
}
- 数组名与指针的区别
int main(void)
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//指向数组的指针
int* p = arr;
//p是变量 arr是常量
//p是一个指针 4个字节大小
//arr是一个数组 40字节大小
printf("指针类型大小:%d\n", sizeof(p));
printf("数组大小:%d\n", sizeof(arr));
//p[i];
//*(p+i);
return 0;
}
- 数组作为函数参数会退化为指针 丢失数组的精度
//比如下面的冒泡排序函数,数组长度参数必须传入,不可以通过arr[]来得到数组的长度,因为数组作为函数参数会退化为指针 丢失数组的精度
void BubbleSort(int arr[],int len)//等价于 void BubbleSort(int* arr,int len)
{
//int len = sizeof(arr);//4或者8
//printf("%d\n", sizeof(arr));
for (int i = 0; i < len-1; i++)
{
for (int j = 0; j < len-1-i; j++)
{
//if (arr[j] > arr[j + 1])
//{
// int temp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = temp;
//}
if (*(arr + j) > *(arr + j + 1))
{
int temp = *(arr + j);
*(arr + j) = *(arr + j + 1);
*(arr + j + 1) = temp;
}
}
}
}
8.3.2 指针运算
-
加法运算
-
指针计算不是简单的整数相加
-
如果是一个int *,+1的结果是增加一个int的大小
-
如果是一个char *,+1的结果是增加一个char大小
-
代码示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> void my_strcpy01(char* dest,char* ch) { int i = 0; //while (ch[i] != '\0'); //while (ch[i] != 0); while (ch[i])//等价于上面两个 { dest[i] = ch[i]; i++; } dest[i] = 0; } void my_strcpy02(char* dest, char* ch) { int i = 0; while (*(ch+i)) { *(dest + i) = *(ch + i); i++; } *(dest + i) = 0; } void my_strcpy03(char* dest, char* ch) { while (*ch) { *dest = *ch; dest++;//指针+1 相当于指向数组下一个元素 内存地址变化了sizeof(char) ch++; } *dest = 0; } void my_strcpy(char* dest, char* ch) { while (*dest++ = *ch++); //解读:首先分析++ * = 三种运算的优先级++高于*(取值运算符)高于= //再分析三种运算的结合性:都是自右向左 //1.先算ch++,dest++但由于++在后面所以先赋值,后面再自增 //2.再进行取值运算*ch,*dest //3.再进行赋值运算将*ch赋值给*dest,注意此时还未自增 //4.赋完值后,进行while语句的判断,判断*dest的值是否非零 //5.判断完后此时才对ch,dest,+1 } int main() { //字符串拷贝 char ch[] = "hello world"; char dest[100]; my_strcpy(dest, ch); printf("%s\n", dest); return EXIT_SUCCESS; }
-
8.3.3 指针数组
指针数组,它是数组,数组的每个元素都是指针类型
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
int main01()
{
//定义数组 数据类型 数据名[元素个数] ={值1,值2}
//定义指针数组
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a,&b,&c };
//arr[0] arr[1] arr[2]
//printf("%d\n", *arr[0]);
for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)
{
printf("%d\n", *arr[i]);
}
printf("指针数组大小:%d\n", sizeof(arr));
printf("指针元素大小:%d\n", sizeof(arr[0]));
return EXIT_SUCCESS;
}
int main02(void)
{
//指针数组里面元素存储的是指针
int a[] = { 1,2,3 };
int b[] = { 4,5,6 };
int c[] = { 7,8,9 };
//指针数组是一个特殊的二维数组模型
//指针数组对应于二级指针
int* arr[] = {a,b,c};
//指针数组和二级指针建立关系
int** p = arr;
//arr是指针数组的首地址
//printf("%p\n", arr);
//printf("%p\n", &arr[0]);
//printf("%p\n", a);
//printf("%d\n", arr[0][1]);
//printf("%p\n", arr[0]);
//printf("%p\n", a);//a[1]
//printf("%p\n", &a[0]);//a[1]
//for (int i = 0; i < 3; i++)
//{
// printf("%d\n", *arr[i]);
//}
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
//二维数组
//printf("%d ", arr[i][j]);
//printf("%d ", *(arr[i] + j));
printf("%d ", *(*(arr + i) + j));//三个式子等价
}
puts("");
}
return 0;
}
8.3.4 多级指针
-
C语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级指针
-
二级指针就是指向一个一级指针变量地址的指针
-
代码示例:
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> int main0401() { int a[] = { 1,2,3 }; int b[] = { 4,5,6 }; int c[] = { 7,8,9 }; //指针数组是一个特殊的二维数组模型 //指针数组对应于二级指针 int* arr[] = { a,b,c }; //指针数组和二级指针建立关系 int** p = arr; //arr[0][0] a[0] //printf("%d\n", **p); //二级指针加偏移量 相当于跳过了一个一维数组大小 //printf("%d\n", **(p + 1)); //一级指针加偏移量 相当于跳过了一个元素 //printf("%d\n", *(*p + 1));//arr[0][1] //printf("%d\n", *(*(p + 1) + 1)); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { //printf("%d ", p[i][j]); //printf("%d ", *(p[i] + j)); //printf("%d ", *(*(p + i) + j)); } puts(""); } return EXIT_SUCCESS; } int main0402(void) { int a = 10; int b = 20; int* p = &a; int** pp = &p; int*** ppp = &pp; //*ppp==pp==&p //**ppp==*pp==p==&a; //***ppp==**pp==*p==a //*pp = &b;//等价于p=&b; **pp = 100; //*pp = 100;//err printf("%d\n", *p); printf("%d\n", a); return 0; }
8.4 指针与函数
8.4.1 指针作为函数参数
//指针作为函数参数
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int a = 10;
int b = 20;
//值传递 形参不影响实参的值
//swap(a, b);
//地址传递 形参可以改变实参的值
swap(&a, &b);
printf("%d\n", a);
printf("%d\n", b);
return EXIT_SUCCESS;
}
8.4.2 数组名(指针)作为函数参数
-
字符串追加问题
#include<stdio.h> //数组运算 void my_strcat01(char* ch1, char* ch2) { //strlen(ch1); int i = 0; while (ch1[i] != '\0') { i++; } int j = 0; while (ch2[j] != '\0') { ch1[i + j] = ch2[j]; j++; } } //指针加偏移量 void my_strcat02(char* ch1, char* ch2) { int i = 0; while (*(ch1 + i) != '\0') { i++; } int j = 0; while (*(ch2 + j) != '\0') { *(ch1 + i + j) = *(ch2 + j); j++; } } //指针运算 void my_strcat03(char* ch1, char* ch2) { while (*ch1)ch1++; while (*ch2) { *ch1 = *ch2; ch1++; ch2++; } } //指针运算的优化 void my_strcat(char* ch1, char* ch2) { while (*ch1)ch1++; while (*ch1++ = *ch2++); } int main() { char ch1[100] = "hello"; char ch2[] = "world"; my_strcat(ch1, ch2); printf("%s\n", ch1); return EXIT_SUCCESS; }
-
去除字符串中的空格
//思路:创建一个空字符数组,将需要处理的字符串中的非空格字符复制到空字符数组中 void remove_space01(char* ch) { char str[100] = {0}; char* temp = str; int i = 0; int j = 0; while (ch[i] != '\0') { if (ch[i] != ' ') { str[j] = ch[i]; j++; } i++; } while (*ch++ = *temp++); } //去除字符串中的空格 //思路: 可以使用两个指针开始时都指向字符串开始位置,一个指针遍历整个字符串,找到不是空格的字符,将其赋值给另一个指针, //然后另一个指针往后移一个位置,直到遍历到\0字符。 void remove_space(char* ch) { //用来遍历字符串 char* ftemp = ch; //记录非空格字符串 char* rtemp = ch; while (*ftemp) { if (*ftemp != ' ') { *rtemp = *ftemp; rtemp++; } ftemp++; } *rtemp = 0; } int main(void) { char ch[] = " h e ll o w o r lld "; remove_space(ch); printf("%s\n", ch); return 0; }
8.4.3 指针作为函数返回值
-
查询某个字符出现在字符串中的位置
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> char* my_strchr01(char* str,char ch) { int i = 0; while (str[i]) { if (str[i] == ch) { return &str[i]; } i++; } return NULL; } char* my_strchr(char* str, char ch) { while (*str) { if (*str == ch) return str; str++; } return NULL; } int main() { char str[] = "hell worldll"; char* p = my_strchr(str, 'o'); if (p == NULL) { printf("未找到\n"); } else { printf("%s\n", p); } return EXIT_SUCCESS; }
-
查询某个子串出现在字符串中的位置
#include <stdio.h> char* mystr_str(char* src,char* dest){ //定义三个临时指针 char* fsrc=src;//用于遍历源字符串 char* tsrc=src;//用于记录可能匹配到目标字符串的字符位置 char* tdest=dest;//用于遍历目标字符串 while(*fsrc){ tsrc=fsrc; while(*fsrc==*tdest&&*fsrc!=0){ fsrc++; tdest++; } if(*tdest==0){ return tsrc; } fsrc=tsrc; tdest=dest; fsrc++; } return NULL; } int main(){ char ch[]="hellewordllo"; char dest[]="llo"; char* p =mystr_str(ch,dest); printf("%s\n",p); printf("%d",p-ch); return 0; }
8.5 指针与字符串
8.5.1 字符串的表现形式及区别
-
用字符数组存放一个字符串
//用字符数组存放一个字符串 char str[]="hello tom";//栈区字符串 char str2[] = {'h','e'};
-
用字符指针指向一个字符串
char* pStr="hello";//数据区常量区字符串
说明:
-
C 语言对字符串常量" hello"是按字符数组处理的,在内存中开辟了一个字符数组用来存放字符串常量,程序在定义字符串指针变量 str 时只是把字符串首地址(即存放字符串的字符数组的首地址)赋给 pStr
-
对应的内存布局图
-
-
区别
-
字符数组由若干个元素组成,每个元素放一个字符;而字符指针变量中存放的是地址(字符串/字符数组的首地 址),绝不是将整个字符串放到字符指针变量中(是字符串首地址)
-
对字符数组只能对各个元素赋值,不能用以下方法对字符数组赋值
char str[14]; str = "hello tom"; //错误 str[0] = 'i'; //ok
-
对字符指针变量,采用下面方法赋值, 是可以的
char* a="yes"; a="hello tom"; *(a + 1) = 'o';//错误
-
8.5.2 字符串数组和指针数组
int main(void)
{
//指针数组
char ch1[] = "hello";
char ch2[] = "world";
char ch3[] = "dabaobei";
char* arr[] = { ch1, ch2, ch3 };
//字符串数组
char* arr[] = { "hello","world","dabaobei" };
char** p = arr;//ok
printf("%c\n", *(*(p+1)+1));//输出o
//arr[0] arr[1] arr[2]
for (int i = 0; i < 3; i++)
{
printf("%s\n", arr[i]);
}
}
8.6 常用字符串应用模型
8.6.1 查找子字符串在字符串中出现的次数
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
char* my_strstr(char* src, char* dest)
{
char* fsrc = src;//用作于循环遍历的指针
char* rsrc = src;//记录每次相同的首地址
char* tdest = dest;
while (*fsrc)
{
rsrc = fsrc;
while (*fsrc == *tdest && *fsrc != '\0')
{
fsrc++;
tdest++;
}
if (*tdest == '\0')
{
return rsrc;
}
//回滚
fsrc = rsrc;
tdest = dest;
fsrc++;
}
return NULL;
}
int main()
{
char str[] = "11abcd111122abcd333abcd3322abcd3333322qqq";
char ch[] = "abcd";
char* p = my_strstr(str, ch);
printf("%d\n", p - str);
int count = 0;//记录个数
while (p)
{
count++;
p += strlen(ch);
p = my_strstr(p, ch);
if(p)printf("%d\n", p-str);//输出出现的位置
}
printf("abcd在字符串中出现:%d次\n", count);
return 0;
}
8.6.2 字符串逆序
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
void inverse01(char* ch)
{
int i = 0;
int j = strlen(ch) - 1;
while (i < j)
{
char temp = ch[i];
ch[i] = ch[j];
ch[j] = temp;
i++;
j--;
}
return;
}
void inverse(char* ch)
{
char* ftemp = ch;
char* btemp = ch + strlen(ch) - 1;
//printf("%c\n", *btemp);
while (ftemp < btemp)
{
char temp = *ftemp;
*ftemp = *btemp;
*btemp = temp;
ftemp++;
btemp--;
}
return;
}
int main0701()
{
char ch[] = "hello world";
inverse(ch);
printf("%s\n", ch);
return EXIT_SUCCESS;
}
//回文字符串
//abcba abccba abcbdcba
int symm(char* ch)
{
char* ftemp = ch;
char* btemp = ch + strlen(ch) - 1;
while (ftemp < btemp)
{
if (*ftemp != *btemp)
return 1;
ftemp++;
btemp--;
}
return 0;
}
int main(void)
{
char ch[] = "abcbdcba";
int value = symm(ch);
if (!value)
{
printf("相同\n");
}
else
{
printf("不相同\n");
}
return 0;
}
8.7 字符串处理函数
8.7.1 strcpy()
-
函数原型 char *strcpy(char *dest, const char *src);
-
功能:把src所指向的字符串复制到dest所指向的空间中,'\0'也会拷贝过去
-
参数:
- dest:目的字符串首地址
- src:源字符首地址
-
返回值:
- 成功:返回dest字符串的首地址
- 失败:NULL
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> void my_strcpy(char* dest,const char* src) { while (*dest++ = *src++); } void my_strncpy(char* dest, const char* src,size_t n) { while ((*dest++ = *src++) && --n);//注意这里是--n } int main() { char ch[] = "hello world"; char str[100] = {0}; //my_strcpy(str, ch); //字符串拷贝 //strcpy(str, ch); //字符串有限拷贝 //strncpy(str, ch, 50); my_strncpy(str, ch, 5); printf("%s\n", str); return EXIT_SUCCESS; }
注意:如果参数dest所指的内存空间不够大,可能会造成缓冲溢出的错误情况。
8.7.2 strncpy()
- 原型:char *strncpy(char *dest, const char *src, size_t n);
- 功能:把src指向字符串的前n个字符复制到dest所指向的空间中,是否拷贝结束看指定的长度是否包含'\0'。
- 参数:
- dest:目的字符串首地址
- src:源字符首地址
- n:指定需要拷贝字符串个数
- 返回值:
- 成功:返回dest字符串的首地址
- 失败:NULL
8.7.3 strcat()
-
原型:char *strcat(char *dest, const char *src);
-
功能:将src字符串连接到dest的尾部,‘\0’也会追加过去
-
参数:
- dest:目的字符串首地址
- src:源字符首地址
-
返回值:
- 成功:返回dest字符串的首地址
- 失败:NULL
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> void my_strcat(char* dest, const char* src) { //找到dest字符串中\0位置 while (*dest)dest++; while (*dest++ = *src++); } void my_strncat(char* dest, char* src, size_t n) { while (*dest)dest++; while ((*dest++ = *src++)&& --n); } int main09() { char dest[100] = "hello"; char src[] = "world"; //字符串追加 //strcat(dest, src); //字符串有限追加 //strncat(dest, src, 30); //my_strcat(dest, src); my_strncat(dest, src, 3); printf("%s\n", dest); return EXIT_SUCCESS; }
8.7.4 strncat()
- 原型:char *strncat(char *dest, const char *src, size_t n);
- 功能:将src字符串前n个字符连接到dest的尾部,‘\0’也会追加过去
- 参数:
- dest:目的字符串首地址
- src:源字符首地址
- n:指定需要追加字符串个数
- 返回值:
- 成功:返回dest字符串的首地址
- 失败:NULL
8.7.5 strcmp()
-
原型:int strcmp(const char *s1, const char *s2);
-
功能:比较 s1 和 s2 的大小,比较的是字符ASCII码大小。
-
参数:
- s1:字符串1首地址
- s2:字符串2首地址
-
返回值:
- 相等:0
- 大于:>0 在不同操作系统strcmp结果会不同 返回ASCII差值
- 小于:<0
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> int my_strcmp(const char *s1, const char *s2) { while (*s1 == *s2) { if (!*s1) return 0; s1++; s2++; } return *s1 > *s2 ? 1 : -1; } int my_strncmp(const char *s1, const char *s2, size_t n) { for (int i = 0; i < n && s1[i] && s2[i]; i++) { if (s1[i] != s2[i]) { return s1[i] > s2[i] ? 1 : -1; } } return 0; } //int my_strcmp(const char *s1, const char *s2) //{ // while (*s1++ == *s2++ && !*s1); // if (!*--s1 && !*--s2)return 0; // return *s1 > *s2 ? 1 : -1; //} int main10() { char ch1[] = "hallo world"; char ch2[] = "hello world"; //int value = strcmp(ch1, ch2); //int value = strncmp(ch1, ch2, 15); //int value = my_strcmp(ch1, ch2); //int value = my_strncmp(ch1, ch2,3); //printf("%d\n", value); if (strcmp(ch1, ch2)) { printf("不相同\n"); } else { printf("相同\n"); } return EXIT_SUCCESS; }
8.7.6 strncmp()
- 原型:int strncmp(const char *s1, const char *s2, size_t n);
- 功能:比较 s1 和 s2 前n个字符的大小,比较的是字符ASCII码大小。
- 参数:
- s1:字符串1首地址
- s2:字符串2首地址
- n:指定比较字符串的数量
- 返回值:
- 相等:0
- 大于: > 0
- 小于: < 0
8.7.7 strchr()
-
原型:char *strchr(const char *s, int c);
-
功能:在字符串s中查找字母c出现的位置
-
参数:
- s:字符串首地址
- c:匹配字母(字符)
-
返回值:
- 成功:返回第一次出现的c地址
- 失败:NULL
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> char* my_strchr(const char* s,int c) { while (*s) { if (*s == c) return s; s++; } return NULL; } int main() { char ch[] = "hello world"; char c = 'l'; //char* p = strchr(ch, c); char* p = my_strchr(ch, c); printf("%s\n", p); return EXIT_SUCCESS; } int main(void) { char ch[] = "hello world"; char str[] = "llo"; char* p = strstr(ch, str); printf("%s\n", p); return 0; }
8.7.8 strstr()
- 原型:char *strstr(const char *haystack, const char *needle);
- 功能:在字符串haystack中查找字符串needle出现的位置
- 参数:
- haystack:源字符串首地址
- needle:匹配字符串首地址
- 返回值:
- 成功:返回第一次出现的needle地址
- 失败:NULL
8.7.9 strtok()
-
原型:char *strtok(char *str, const char *delim);
-
功能:能将字符串分割成一个个片段。当strtok()在参数s的字符串中发现参数delim中包含的分割字符时, 则会将该字符改为\0 字符,当连续出现多个时只替换第一个为\0。
-
参数:
- str:指向欲分割的字符串
- delim:为分割字符串中包含的所有字符
-
返回值:
- 成功:分割后字符串首地址
- 失败:NULL
注意:在第一次调用时:strtok()必需给予参数s字符串;往后的调用则将参数s设置成NULL,每次调用成功则返回指向被分割出片段的指针
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> int main1() { //字符串截取 strtok 会破坏源字符串 用\0替换分割的标志位 char ch[] = "www.itcast.cn";//123456@qq.com //www\0itcast.cn //www\0itcast\0cn char* p = strtok(ch, "."); printf("%s\n", p);//www //剩下的字符串在缓冲区中,可以用NULL来再次使用 p = strtok(NULL, "."); printf("%s\n", p); p = strtok(NULL, "."); printf("%s\n", p); //printf("%s\n", ch); //printf("%p\n", p); //printf("%p\n", ch); return EXIT_SUCCESS; } int main2(void) { char ch[] = "123456@qq.com"; char str[100] = { 0 }; //字符串备份 strcpy(str, ch); char* p = strtok(str, "@"); printf("%s\n", p); p = strtok(NULL, "."); printf("%s\n", p); return 0; } int main3(void) { //char ch[] = "nichousha\nchounizadi\nzaichouyigeshishi\nduibuqidagewocuole\nguawazi"; char ch[] = "你瞅啥\n瞅你咋啦\n再瞅一个试试\n对不起大哥我错喽\n瓜娃子"; char* p = strtok(ch, "\n"); while (p) { printf("%s\n", p); p = strtok(NULL, "\n"); } return 0; }
8.7.10 atoi()
-
原型:int atoi(const char *nptr);
-
功能:atoi()会扫描nptr字符串,跳过前面的空格字符,直到遇到数字或正负号才开始做转换,而遇到非数字或字符串结束符('\0')才结束转换,并将结果返回返回值。
-
参数:nptr:待转换的字符串
-
返回值:成功转换后整数
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> int main1() { char ch[] = " -123-456abc123"; int i = atoi(ch); printf("%d\n", i);//-123 return EXIT_SUCCESS; } int main2() { char ch[] = " -123.456-456abc123"; double i = atof(ch); printf("%.2f\n", i);//-123.46 return EXIT_SUCCESS; } int main() { char ch[] = " -123.456-456abc123"; long i = atol(ch); printf("%ld\n", i); return EXIT_SUCCESS; }
-
类似的函数有:
- atof():把一个小数形式的字符串转化为一个浮点数。
- atol():将一个字符串转化为long类型
九、内存管理
9.1 作用域
C语言变量的作用域分为:
-
代码块作用域(代码块是{}之间的一段代码)
-
函数作用域
-
文件作用域
9.1.1 局部变量与全局变量
-
局部变量:局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块{}内部定义的变量都是自动变量,它有如下特点
-
在一个函数内定义,只在函数范围内有效
-
在复合语句中定义,只在复合语句中有效
-
随着函数调用的结束或复合语句的结束局部变量的声明声明周期也结束
-
如果没有赋初值,内容为随机
-
示例:
#include <stdio.h> void test() { //auto写不写是一样的 //auto只能出现在{}内部 auto int b = 10; } int main(void) { //b = 100; //err, 在main作用域中没有b if (1) { //在复合语句中定义,只在复合语句中有效 int a = 10; printf("a = %d\n", a); } //a = 10; //err离开if()的复合语句,a已经不存在 return 0; }
-
-
全局变量
-
在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明
-
全局变量的生命周期和程序运行周期一样
-
不同文件的全局变量不可重名
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> //数据区 全局变量可以和局部变量重名 //全局变量 在函数外部定义的变量 //作用域:整个项目中所有文件 如果在其他文件中使用 需要声明 //生命周期:从程序创建到程序销毁 int a = 10; void fun02() { a = 100; printf("%d\n", a); } int main02() { //数据在操作时会采用就进原则 printf("%d\n", a); int a = 123; printf("%p\n", &a); //匿名内部函数 { int a = 456;//这是是匿名内部函数新定义的局部变量不会影响外面的a //a = 456;如果这将456赋值给a改变的是主函数内定义的a,也即将123改成了456,后面输出就是456 printf("%p\n", &a); printf("%d\n", a); } printf("%d\n", a); fun02(); return EXIT_SUCCESS; }
-
9.1.2 静态变量(static)
- 静态局部变量
- static局部变量的作用域也是在定义的函数内有效
- static局部变量的生命周期和程序运行周期一样,同时staitc局部变量的值只初始化一次,但可以赋值多次
- static局部变量与全局变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符
- 静态全局变量
- 在函数外定义,作用范围被限制在所定义的文件中
- 不同文件静态全局变量可以重名,但作用域不冲突
- static全局变量的生命周期和程序运行周期一样,同时staitc全局变量的值只初始化一次
9.1.3 变量总结
注意:同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。
9.1.4 全局函数与静态函数
-
在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。
-
对于不同文件中的staitc函数名字可以相同。
-
总结:
9.2 内存布局
9.2.1 内存分区
C代码经过预处理、编译、汇编、链接4步后生成一个可执行程序。
在 Windows 下,程序是一个普通的可执行文件,以下列出一个二进制可执行文件的基本情况:
通过上图可以得知,在没有运行程序前,也就是说程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)和未初始化数据区(bss)3 个部分(有些人直接把data和bss合起来叫做静态区或全局区)。
-
代码区
存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
-
全局初始化数据区/静态数据区(data段)
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
-
未初始化数据区(又叫 bss 区)
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。
程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。
-
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
-
堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
9.2.2 内存操作函数
9.2.2.1 memset()
-
原型:void *memset(void *s, int c, size_t n);
-
功能:将s的内存区域的前n个字节以参数c填入
-
参数:
- s:需要操作内存s的首地址
- c:填充的字符,c虽然参数为int,但必须是unsigned char , 范围为0~255
- n:指定需要设置的大小
-
返回值:s的首地址
-
示例:
int main() { int* p = (int*)malloc(sizeof(int) * 10); //memset()重置内存空间的值 memset(p, 0, sizeof(int)*10); //memset(p, 1, 40);//如果填入的是1,是按照字节填入int类型占4个字节每个字节是0x01,4个字节就是01010101H打印出来不会是1 for (int i = 0; i < 10; i++) { printf("%d\n", p[i]); } free(p); char ch[10]; //memset(ch, 'A', sizeof(char) * 10);//这样打印出来会乱码,因为不含'\0' memset(ch, 0, sizeof(char)*10); printf("%s\n", ch); return EXIT_SUCCESS; }
9.2.2.2 memcpy()
-
原型:void *memcpy(void *dest, const void *src, size_t n);
-
功能:拷贝src所指的内存内容的前n个字节到dest所值的内存地址上。
-
参数
- dest:目的内存首地址
- src:源内存首地址,注意:dest和src所指的内存空间不可重叠,可能会导致程序报错
- n:需要拷贝的字节数
-
返回值:dest的首地址
-
示例:
int main(void) { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int* p = (int*)malloc(sizeof(int) * 10); //字符串拷贝 strcpy() //内存拷贝 memcpy(p, arr, sizeof(int) * 10); for (int i = 0; i < 10; i++) { printf("%d\n", p[i]); } free(p); char ch[] = "hello\0 world"; char str[100]; //字符串拷贝遇到\0停止 //strcpy(str, ch); //内存拷贝 拷贝的内容和字节有关 memcpy(str, ch, 13); //printf("%s\n", str); for (int i = 0; i < 13; i++) { printf("%c", str[i]);//\0后面的字符也会被拷贝 } int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //如果拷贝的目标和源发生重叠 可能报错 memcpy(&arr[5], &arr[3], 20); for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } return 0; }
9.2.2.3 memmove()
memmove()功能用法和memcpy()一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy()低些。
示例:
int main(void)
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//如果拷贝的目标和源发生重叠 可能报错
//memcpy(&arr[5], &arr[3], 20);
memmove(&arr[5], &arr[3], 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
}
9.2.2.4 memcmp()
-
原型:int memcmp(const void *s1, const void *s2, size_t n);
-
功能:比较s1和s2所指向内存区域的前n个字节
-
参数:
- s1:内存首地址1
- s2:内存首地址2
- n:需比较的前n个字节
-
返回值:
- 相等:=0
- 大于:>0
- 小于:<0
-
示例:
int main(void) { //int arr1[] = { 1,2,3,4,5,6,7,8,9,10 }; //int arr2[] = { 1,2,3,4,5 }; char arr1[] = "hello\0 world"; char arr2[] = "hello\0 world"; //strcmp(); int value = memcmp(arr1, arr2, 13);//按字节进行比较,可以比较\0后面的字符 printf("%d\n", value); return 0; }
9.2.3 堆区内存分配和释放
9.2.3.1 malloc()
-
原型:void *malloc(size_t size);
-
功能:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
-
参数:size:需要分配内存大小(单位:字节)
-
返回值:
- 成功:分配空间的起始地址
- 失败:NULL
-
示例:
int main() { //栈区大小 1M //int arr[210000] = {0}; //开辟堆空间存储数据 int* p = (int*)malloc(sizeof(int)); printf("%p\n", p); //使用堆空间 *p = 123; printf("%d\n", *p); //释放堆空间 free(p); p = NULL; //p 野指针 //printf("%p\n", p); //*p = 456; //printf("%d\n", *p); return EXIT_SUCCESS; }
9.2.3.2 free()
- 原型:void free(void *ptr);
- 功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。
- 参数:ptr:需要释放空间的首地址,被释放区应是由malloc函数所分配的区域。
- 返回值:无
9.2.4 二级指针对应的堆空间
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
int main()
{
//int arr[5][3]
//开辟二级指针对应的堆空间
int** p = (int**)malloc(sizeof(int*) * 5);
for (int i = 0; i < 5; i++)
{
//开辟一级指针对应的堆空间
p[i] = (int*)malloc(sizeof(int) * 3);
}
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 3; j++)
{
scanf("%d", &p[i][j]);
}
}
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 3; j++)
{
printf("%d ", p[i][j]);
//printf("%d ", *(p[i] + j));
//printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
for (int i = 0; i < 5; i++)
{
free(p[i]);
}
free(p);
return EXIT_SUCCESS;
}
十、复合类型(自定义类型)
10.1 结构体
10.1.1 概述
数组:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。
有时我们需要将不同类型的数据组合成一个有机的整体,如:一个学生有学号/姓名/性别/年龄/地址等属性。显然单独定义以上变量比较繁琐,数据不便于管理。
C语言中给出了另一种构造数据类型——结构体。
10.1.2 结构体变量的定义和初始化
-
定义结构体变量的方式:
- 先声明结构体类型再定义变量名
- 在声明类型的同时定义变量
- 直接定义结构体类型变量(无类型名)
-
结构体类型和结构体变量关系:
- 结构体类型:指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元。
- 结构体变量:系统根据结构体类型(内部成员状况)为之分配空间。
-
示例:
//结构体类型的定义 struct stu { char name[50]; int age; }; //先定义类型,再定义变量(常用) struct stu s1 = { "mike", 18 }; //定义类型同时定义变量 struct stu2 { char name[50]; int age; }s2 = { "lily", 22 }; //直接定义结构体类型变量(无类型名) struct { char name[50]; int age; }s3 = { "yuri", 25 };
10.1.3 结构体成员的使用
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
//struct 结构体名
//{
// 结构体成员列表
// 姓名
// 年龄
// 成绩
//}
struct student
{
char name[21];
int age;
int score;
char addr[51];
};//stu = { "张三",18,100,"北京市昌平区北清路22号" };
int main1()
{
//创建结构体变量
//结构体类型 结构体变量
//struct student stu;
////stu.name = "张三";//不能直接给字符串数组赋值
//strcpy(stu.name, "张三");//可以使用strcpy()函数赋值
//stu.age = 18;
//stu.score = 100;
//strcpy(stu.addr, "北京市昌平区北清路22号");
struct student stu = { "张三",18,100,"北京市昌平区北清路22号" };
printf("姓名:%s\n", stu.name);
printf("年龄:%d\n", stu.age);
printf("成绩:%d\n", stu.score);
printf("地址:%s\n", stu.addr);
return EXIT_SUCCESS;
}
int main2(void)
{
struct student stu;
scanf("%s%d%d%s", stu.name, &stu.age, &stu.score, stu.addr);
printf("姓名:%s\n", stu.name);
printf("年龄:%d\n", stu.age);
printf("成绩:%d\n", stu.score);
printf("地址:%s\n", stu.addr);
return 0;
}
10.1.4 结构体数组
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
struct student
{
//结构体成员需要偏移对齐
char name[21];
int age;
char sex;
int score[3];
char addr[51];
};
int main1()
{
struct student stu[3] =
{
{"黄某航",22,'M',88,99,0,"河北唐山"},
{"马某羊",18,'F',59,59,59,"河北邯郸"},
{"大法师",30,'M',100,100,100,"黑龙江大庆"}
};
//sizeof()计算数据类型在内存中占的字节大小
printf("结构体数组大小:%d\n", sizeof(stu));//不会等于结构体各成员所占内存之和,因为要考虑个成员偏移对齐的情况
printf("结构体元素大小:%d\n", sizeof(stu[0]));
printf("结构体元素个数:%d\n", sizeof(stu) / sizeof(stu[0]));
for (int i = 0; i < 3; i++)
{
printf("姓名:%s\n", stu[i].name);
printf("年龄:%d\n", stu[i].age);
printf("性别:%s\n", stu[i].sex == 'M' ? "男" : "女");
printf("成绩1:%d\n", stu[i].score[0]);
printf("成绩2:%d\n", stu[i].score[1]);
printf("成绩3:%d\n", stu[i].score[2]);
printf("地址:%s\n", stu[i].addr);
}
return EXIT_SUCCESS;
}
int main2(void)
{
struct student stu[3] =
{
{ "黄某航",22,'M',88,99,0,"河北唐山" },
{ "马某羊",18,'F',59,59,59,"河北邯郸" },
{ "大法师",30,'M',100,100,100,"黑龙江大庆" }
};
for (int i = 0; i < 3 - 1; i++)
{
for (int j = 0; j< 3 - 1 - i; j++)
{
if (stu[j].age < stu[j + 1].age)
{
//结构体赋值
struct student temp = stu[j];
stu[j] = stu[j + 1];
stu[j + 1] = temp;
}
}
}
for (int i = 0; i < 3; i++)
{
printf("姓名:%s\n", stu[i].name);
printf("年龄:%d\n", stu[i].age);
printf("性别:%s\n", stu[i].sex == 'M' ? "男" : "女");
printf("成绩1:%d\n", stu[i].score[0]);
printf("成绩2:%d\n", stu[i].score[1]);
printf("成绩3:%d\n", stu[i].score[2]);
printf("地址:%s\n", stu[i].addr);
}
return 0;
}
10.1.5 开辟堆空间存储结构体
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
typedef struct student ss;
struct student
{
//结构体成员需要偏移对齐
char name[21];
int age;
char sex;
int score[3];
char addr[51];
};
int main0401()
{
//printf("%d\n", sizeof(struct student));
ss * p = (ss *)malloc(sizeof(ss) * 3);
printf("结构体指针大小:%d", sizeof(ss*));
for (int i = 0; i < 3; i++)
{
scanf("%s%d,%c%d%d%d%s", p[i].name, &p[i].age, &p[i].sex,
&p[i].score[0], &p[i].score[1], &p[i].score[2], p[i].addr);
}
for (int i = 0; i < 3; i++)
{
printf("姓名:%s\n", p[i].name);
printf("年龄:%d\n", p[i].age);
printf("性别:%s\n", p[i].sex == 'M' ? "男" : "女");
printf("成绩1:%d\n", p[i].score[0]);
printf("成绩2:%d\n", p[i].score[1]);
printf("成绩3:%d\n", p[i].score[2]);
printf("地址:%s\n", p[i].addr);
}
free(p);
return EXIT_SUCCESS;
}
10.1.6 结构体嵌套结构体
#include <stdio.h>
struct person
{
char name[20];
char sex;
};
struct stu
{
int id;
struct person info;
};
int main()
{
struct stu s[2] = { 1, "lily", 'F', 2, "yuri", 'M' };
int i = 0;
for (i = 0; i < 2; i++)
{
printf("id = %d\tinfo.name=%s\tinfo.sex=%c\n", s[i].id, s[i].info.name, s[i].info.sex);
}
return 0;
}
10.1.7 结构体赋值
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
struct student
{
char name[21];
//char* name;//??
int age;
int score;
char addr[51];
};
int main()
{
struct student stu = { "孙尚香",26,60,"巴蜀" };
struct student s1 = stu;
//int a = 10;
//int b = a;
//b = 20;
//深拷贝和浅拷贝
strcpy(s1.name, "甘夫人");
s1.age = 28;
s1.score = 80;
//这里不会改变源结构体的值
//但结构体成员换成char* name就不一样
printf("%s\n", stu.name);//孙尚香
printf("%d\n", stu.age);//26
printf("%d\n", stu.score);//60
return EXIT_SUCCESS;
}
10.1.8 结构体指针
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
//结构体成员为指针类型
struct student
{
char* name;
int age;
int *scores;
char* addr;
};
int main1()
{
struct student stu;
//stu.name = "张三";
stu.name = (char*)malloc(sizeof(char) * 21);
stu.scores = (int*)malloc(sizeof(int) * 3);
stu.addr = (char*)malloc(sizeof(char) * 51);
strcpy(stu.name, "张三");
stu.age = 18;
stu.scores[0] = 88;
stu.scores[1] = 99;
stu.scores[2] = 100;
strcpy(stu.addr, "北京市");
printf("%s\n", stu.name);
printf("%d\n", stu.age);
printf("%d\n", stu.scores[0]);
printf("%d\n", stu.scores[1]);
printf("%d\n", stu.scores[2]);
printf("%s\n", stu.addr);
free(stu.name);
free(stu.scores);
free(stu.addr);
return EXIT_SUCCESS;
}
struct stu
{
char name[21];
int age;
int scores[3];
char addr[51];
};
int main2(void)
{
//结构体指针
struct stu ss = { "林冲",30,100,100,100,"汴京" };
struct stu * p = &ss;
//printf("%s\n", (*p).name);
//printf("%d\n", (*p).age);
//结构体指针->成员
//结构体变量.成员
printf("%s\n", p->name);
printf("%d\n", p->age);
printf("%d\n", p->scores[0]);
printf("%d\n", p->scores[1]);
printf("%d\n", p->scores[2]);
printf("%s\n", p->addr);
return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
typedef struct student ss;
struct student
{
char* name;
int age;
int* scores;
char* addr;
};
int main08()
{
//通过结构体指针操作堆空间
ss* p = (ss*)malloc(sizeof(ss) * 3);
for (int i = 0; i < 3; i++)
{
//(p + i)->name;
p[i].name = (char*)malloc(sizeof(char) * 21);
p[i].scores = (int*)malloc(sizeof(int) * 3);
p[i].addr = (char*)malloc(sizeof(char) * 51);
}
for (int i = 0; i < 3; i++)
{
scanf("%s%d%d%d%d%s", p[i].name, &p[i].age, &p[i].scores[0],
&p[i].scores[1], &p[i].scores[2], p[i].addr);
}
for (int i = 0; i < 3; i++)
{
printf("%s ", p[i].name);
printf("%d ", p[i].age);
printf("%d ", p[i].scores[0]);
printf("%d ", (p + i)->scores[1]);
printf("%d ", (p + i)->scores[2]);
printf("%s\n", (p + i)->addr);
}
//释放堆空间
//先释放内层,再释放外层
for (int i = 0; i < 3; i++)
{
free(p[i].name);
free(p[i].scores);
free(p[i].addr);
}
free(p);
system("pause");
return EXIT_SUCCESS;
}
10.1.9 结构体与函数
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
typedef struct student ss;
struct student
{
char name[21];
//char *name;
int age;
int score[3];
char addr[51];
};
void fun01(ss stu1)
{
//stu1.name = (char*)malloc(sizeof(char)*21);
strcpy(stu1.name, "卢俊义");
printf("%s\n", stu1.name);
}
int main01()
{
ss stu = { NULL,50,101,"水泊梁山" };
//stu.name = (char*)malloc(sizeof(char) * 21);
strcpy(stu.name, "宋江");
fun01(stu);
printf("%s\n", stu.name);
return EXIT_SUCCESS;
}
void fun02(ss * p)
{
strcpy(p->name, "公孙胜");
printf("%s\n", p->name);
}
int main02(void)
{
//结构体指针作为函数参数
ss stu = { "吴用",50,101,"水泊梁山" };
fun02(&stu);
printf("%s\n", stu.name);
return 0;
}
//数组作为函数参数退化为指针 丢失元素精度 需要传递个数
void BubbleSort(ss * stu, int len)
{
//printf("%d\n", sizeof(stu));
for (int i = 0; i < len - 1; i++)
for (int j = 0; j < len - i - 1; j++)
{
//if (stu[j].age>stu[j + 1].age)
if ((stu + j)->age > (stu + j + 1)->age)
{
ss temp = stu[j];
stu[j] = stu[j + 1];
stu[j + 1] = temp;
}
}
}
int main03(void)
{
ss stu[3] =
{
{ "鲁智深",30,33,33,33,"五台山" },
{"呼延灼",45,44,44,44,"汴京"},
{"顾大嫂",28,33,33,33,"汴京"},
};
BubbleSort(stu, 3);
for (int i = 0; i < 3; i++)
{
printf("姓名:%s\n", stu[i].name);
printf("年龄:%d\n", stu[i].age);
printf("成绩1:%d\n", stu[i].score[0]);
printf("成绩2:%d\n", stu[i].score[1]);
printf("成绩3:%d\n", stu[i].score[2]);
printf("地址:%s\n", stu[i].addr);
}
}
10.1.10 const修饰的结构体指针
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
#include<time.h>
typedef struct student ss;
struct student
{
char name[21];
//char *name;
int age;
int score[3];
char addr[51];
};
int main1001()
{
ss stu1 = { "孙悟空",700,101,101,101,"花果山" };
ss stu2 = { "猪八戒",1200,1000,1000,1000,"高老庄" };
//const修饰结构体指针类型
const ss* p = &stu1;
//p = &stu2;//OK
//p->age = 888;//err
//(*p).age = 888;//err
return EXIT_SUCCESS;
}
int main1003(void)
{
ss stu1 = { "孙悟空",700,101,101,101,"花果山" };
ss stu2 = { "猪八戒",1200,1000,1000,1000,"高老庄" };
//const 修饰结构体指针变量
ss* const p = &stu1;
//p = &stu2;//err
p->age = 888;
strcpy(p->name, "沙悟净");
}
int main1004(void)
{
ss stu1 = { "孙悟空",700,101,101,101,"花果山" };
ss stu2 = { "猪八戒",1200,1000,1000,1000,"高老庄" };
//const 修饰结构体指针类型
//const 修饰结构体指针变量
const ss* const p = &stu1;
//p = &stu2;//err;
//p->age = 888;//err
ss** pp = &p;
//*pp = &stu2;
(*pp)->age = 888;
(**pp).age = 888;
}
10.2 共用体(联合体)
-
联合union是一个能在同一个存储空间存储不同类型数据的类型
-
联合体所占的内存长度等于其最长成员的长度倍数,也有叫做共用体
-
同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用
-
共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖
-
共用体变量的地址和它的各成员的地址都是同一地址
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> union Var { int a; float b; double c; char d; short f[6];//12 }; int main11() { union Var var; var.a = 100; var.b = 3.14; printf("%d\n", var.a); printf("%f\n", var.b); printf("大小:%d\n", sizeof(var));//16个字节 printf("%p\n", &var); printf("%p\n", &var.a); printf("%p\n", &var.b); printf("%p\n", &var.c); return EXIT_SUCCESS; }
10.3 枚举
-
枚举:将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。
-
枚举类型定义:
enum 枚举名 { 枚举值表 };
-
在枚举值表中应列出所有可用值,也称为枚举元素
-
枚举值是常量,不能在程序中用赋值语句再对它赋值
-
枚举元素本身由系统定义了一个表示序号的数值从0开始顺序定义为0,1,2 …
-
示例:
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #include<time.h> enum TYPE { run,attack,skill,dance=10,showUI,frozen=20,dizz,dath,moti=30 }; int main() { int value; while (1) { scanf("%d", &value); switch (value) { case run: printf("英雄正在移动中...\n"); //value = 30; break; case attack: printf("英雄正在攻击中...\n"); break; case skill: printf("英雄正在释放技能中...\n"); break; case dance: printf("英雄正在跳舞中...\n"); break; case showUI: printf("英雄正在显示徽章...\n"); break; case frozen: printf("英雄被冰冻中...\n"); break; case dizz: printf("英雄被眩晕中...\n"); break; case dath: printf("英雄死亡...\n"); return 0; break; case moti: printf("英雄等待释放命令...\n"); break; } } return EXIT_SUCCESS; }
10.4 typedef
typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型。
- 与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
-
define发生在预处理,typedef发生在编译阶段
示例:
#include <stdio.h>
typedef int INT;
typedef char BYTE;
typedef BYTE T_BYTE;
typedef unsigned char UBYTE;
typedef struct type
{
UBYTE a;
INT b;
T_BYTE c;
}TYPE, *PTYPE;
//这里相当于这样定义
//typedef struct type TYPE
//typedef struct type* PTYPE
int main()
{
TYPE t;
t.a = 254;
t.b = 10;
t.c = 'c';
PTYPE p = &t;
printf("%u, %d, %c\n", p->a, p->b, p->c);
return 0;
}