C 语言的知识点不多,但是比较杂。本文系统地对 C 语言进行补充学习。
001.C语言的简洁 32个关键字、9中控制语句、34个运算符。
short int float double char unsigned long enum void auto for while do if else switch case break continue goto return sizeof const typedef static struct default union register signed extern volatile
If-else for while do-while continue break switch goto Return
002.编译过程 1 2 3 4 gcc hello.c gcc hello.c -o hello gcc -o hello hello.c
1.分步编译的四步 **预处理:**宏定义展开、头文件展开、条件编译、删除注释、不会检查语法错误,生成 *.i 文件
。 **编译:**词法分析、语法分析、语义分析、优化后生成相应的汇编代码。检查语法错误,将 *.i 文件
编译成汇编文件 *.s 文件
。 **汇编:**将 *.s 文件
生成 目标文件*.o 文件
(二进制文件)。 **链接:**C语言程序需要依赖各种库,编译后需要把库链接到可执行文件*.out
或者 *.exe
中。【主要是动态库即DLL文件(Dynamic Link Library)】。 2.编译命令 1 2 3 4 gcc -E hello.c -o hello.i gcc -S hello.i -o hello.s gcc -c hello.s -o hello.o gcc hello.o -o hello
选项 含义 -E 只进行预处理 -S 只进行预处理、编译 -c 只进行预处理、编译、汇编 -o 指定输出文件的文件名
PS:助记,选项是ESC,后缀是iso。
3.文件包含处理 include <>
用于包含库函数的头文件。
include ""
用于包含自定义函数的头文件。
4.条件编译 条件编译在预处理阶段展开。
(1)测试存在 1 2 3 4 5 #ifdef 标示符 程序段1 ;#else 标示符 程序段2 ;#endif
举个例子:
如果存在变量 a,则打印上面一句,如果不存在变量a,则打印下面一句。
1 2 3 4 5 6 7 8 9 10 int main () { int a;#ifdef a printf ("存在 变量a" );#else printf ("不存在 变量a" ); #endif }
(2)测试不存在 ifndef
比 ifdef
多一个 n。
多用于 防止头文件重复包含。
1 2 3 4 5 #ifndef 标示符 程序段1 ;#else 标示符 程序段2 ;#endif
举个例子:
自定义头文件 test.h
。为防止头文件重复包含,可以使用以下条件编译:
1 2 3 #ifndef _TEST_H_ #define _TEST_H_ #endif
也可以防止头文件重复包含。
(3)根据表达式定义 1 2 3 4 5 #if 表达式 程序段1 ;#else 程序段2 ;#endif
举个例子:
用来调试代码。
003.system函数 **用途:**在程序中,执行DOS命令 / Linux命令 /外部程序。
常见的有:
system(“PAUSE”)
暂停屏幕
system(“CLS”)
:清屏
system("shutdown -s -t 3")
:三秒后关机
1 2 3 4 5 6 7 #include <stdio.h> #include <stdlib.h> int main () { printf ("Command is coming:" ); system("ls" ); system("./hello" ) }
PS:Windows的两个框架:QT / MFC,故可用QT / MFC调用system函数,执行 DOS命令 或者 外部程序。
004.数据类型 1.作用 编译器预算变量分配的内存空间大小。
2.本质 数据类型的本质:固定内存大小的别名
3.补充 变量命名不能以数字开头。 char 类型本质上是 1字节大小的整型,存储的是ASCII码。 4.数组类型 数组也是一种数据类型,由 元素个数 和 元素对应类型 决定。
int a[10];
,a 的数据类型是 int [10]
。
5.sizeof (1)sizeof是用来求数据类型的字节数的。 如 int a;
那么无论 sizeof(int)
或者是 sizeof(a)
都是等于4,因为 sizeof(a)
其实就是 sizeof(type of a)
。
对于 char str[] = "ab";
就有 sizeof(str) == 3
, 因为 str 的数据类型是 char [3]
。 对于 sizeof("ab") == 3
,因为 "ab"
的数据类型是 const char [3]
。 对于char *p = "ab";
就有 sizeof(p) == 4
,因为 p 的数据类型是 char*
。 对于空字符串 sizeof("") == 1
,sizeof("\0") == 2
。只要是字符串,默认结尾加 \0
(2)数组名和指针的辨析 1 2 3 4 5 6 7 8 9 10 char *p1 = "ab" ; char *p2;char s[2 ] = "ab" ; p2 = s; sizeof (s); sizeof (&s); sizeof (p1); sizeof (p2); sizeof ("ab" );
(3)内存大小和字符串长度的辨析 sizeof 计算内存大小,测量字符串长度,使用 strlen()
函数。
1 2 char a[100 ] = {'a' , 'b' , 'c' }; char b[] = “abc”;
6.数据类型取值范围 计算机储存数据,都是按照补码来储存的,所以取值范围即若干位补码的取值范围。
(1)字节大小 数据类型 占用空间 取值范围 char 1字节 -128~127(-27 ~ 27 -1) short 2字节 -32768~32767(-215 ~ 215 -1) int 4字节 -2147483648~2147473647(-231 ~ 231 -1) long 4字节 -2147483648~2147473647(-231 ~ 231 -1) unsigned short 2字节 0~65535(0 ~ 216 -1) unsigned int 4字节 0~4294967295(0 ~ 232 -1) unsigned long 4字节 0~4294967295(0 ~ 232 -1) long long 8字节 float 4字节 精度:7位有效数字 double 8字节 精度:16位有效数字
(2)为何正数范围比负数范围少一个? 以有符号的char为例。
原码范围 正数 0000 0000 ~ 0111 111 +0 ~ +127 负数 1000 0000 ~ 1111111 -0 ~ -127 特殊 1000 0000 不等于-0,等于-128 -128
**原因:**因为-127 + -1 = 1 1000 0000
即-128,舍去符号位1,将 1000 0000
当作 -128。
1 2 3 1000 0001 (-127 的补码) 1111 1111 (-1 的补码)1 1000 0000 (-127 + -1 = -128 的补码)
(3)数值越界 化为补码运算后,舍弃高位。
1 2 unsigned char a = 255 + 2 ;printf ("%u" , a);
PS:以下不是数值越界情况,a + 2
是另外一块存储单元。
1 2 unsigned char a = 255 ;printf ("%u" , a + 2 );
7.关键字:signed、unsigned 1 signed int a = -1 ; int a = -1 ;
unsigned 表示 无符号。 无符号打印用 %u
,打印的是补码,所以不能打印负数!。 1 2 unsigned int a = 1 ; print("%u" , a);
005.输出 1.格式 类型 格式 short %hd long %ld unsigned int %u long %ld long long %lld unsigned long long %ull 以16进制打印指针 %p 打印% %%
转义字符 含义 \r 光标切换到句首,【删除本行】 用处:进度条,倒计时 \b 退格,【删除一个字符】 \123 八进制转义字符 \x23 十六进制转义字符 “\0123” 字符串中 “\0123” 表示 \n,
1 2 printf ("%d" , '\123' ); printf ("%d" , '\0x23' );
double 和 float 输出默认六位小数,格式化输出除外。 2.调试 用于打印调试日志。
__FILE__
、__LINE__
是系统设置好的。⚠️注意:__FILE__
、__LINE__
是左右各两个下划线连在一起。
1 2 printf ("file = %s\n" , __FILE__); printf ("line = %d\n" , __LINE__);
3.补充:取余和取模的区别 举例:
1 2 3 4 5 6 7 -7 % 4 = -3 7 % -4 = 3 -7 mod 4 = 1 7 mod -4 = -1
**本质:**取余和取模都是以下两步
1 2 商 = a / b; 取余或取模 = a - (商 * b);
取余和取模的区别就在于第一步求商这步:
取余:-7 / 4 = -1.75,取余会靠近 0 舍入,商 = -1。 取模:-7 / 4 = -1.75,取模会靠近 负无穷大 舍入,商 = -2。 第二步相同。
006.各进制的储存方式 十进制DEC 直接写 %d 八进制OCT 以0开头:int a =0123 %o【默认以四个字节输出】 十六进制HEX 以0x开头:int a = 0x123 %x【默认以四个字节输出】 %X(即大写字母十六进制输出) 二进制BIN C语言不能直接表示
注意事项:
举例:
1 2 3 char a = 0x81 ; pritnf("%d" , a);
1 2 3 int a = -1 ; printf ("%0x" , a)
PS:%0x
以四个字节输出,(32位)。
007.关键字:define、类型限定符 **类型限定符:**extern、const、volatile、register。
1.define 作用:定义一个宏定义标示符MAX,它代表1000。 #号开头的语句是预处理语句,(预处理时,MAX将自动替换为1000),无需分号结束。 2.extern int a 是既声明,又定义。 extern int a 是声明,未定义(即未建立储存空间)。 3.const constant 的缩写。
(1)修饰只读变量 (2)易错 1 2 const int a; const int b = 10 ;
1 2 extern const int i; extern const int j=10 ;
(3)好处 可以避免不必要的内存分配。(define 有若干个拷贝,const 只有一份拷贝,不会浪费内存。) 指针做函数参数,可以有效的提高代码可读性,减少bug。 4.volatile 防止编译器优化代码。
1 2 3 int i = 10 ;volatile int a = i;int b = i;
编译器优化代码:当编译器发现,第2行 和 第3行之间没有代码对 i 的值进行改变,自动把上次读的 i 值 放在 b 中。 volatile
关键字声明 变量i 之后,告诉编译器 i 易变(受操作系统、硬件或者其它线程的影响), 编译器每次都需要在 i 的地址处读取 i 值。5.register register:寄存器
定义寄存器变量,如果CPU有空闲寄存器,将a存入寄存器,提高效率。 008.switch语句 case 数字或者字符
1 2 3 4 5 6 7 8 switch (a) { case 1 : pass; case 2 : pass; default : pass; }
009.随机数 void srand(unsigned int seed);
设置rand()产生随机数时的随机种子seed。int rand(void)
rand()返回一个随机数。1 2 3 4 #include <stdlib.h> #include <time.h> srand((unsigned int )time(NULL )); random_num = rand();
010.getchar() 吞掉回车 读入字符的时候,一定要注意!利用getchar() 清空 stdin 的缓冲区。
【两种场景 】
先读入一个字符串,再读入一个字符 1 2 3 4 5 scanf ("%s" , str); ... ... sacnf("%c" , ch);
先读入一个字符,再读入一个字符 1 2 3 4 5 scanf ("%c" , ch1); ... ... sacnf("%c" , ch2);
【原因 】
键盘输入的字符,会放到缓冲区,scanf
函数从缓冲区中读取。读取字符的时候,可以读 ‘\n’
。读字符串时,不会读入\n
,所以不需要 getchar()
。
以两个c为例,键盘上敲出,
缓冲区中显示如下。
scanf(“%c”, ch)
会读取一个 \n
。
011.字符数组和字符串 1.区别 在 C 语言中,字符串实际上是使用 null 字符 ‘\0’ 终止的一维字符数组。
1 2 3 4 5 6 7 8 9 10 11 12 char a[] = {'a' , 'b' , 'c' }; char a[3 ] = "abc" ; char a[] = {'a' , 'b' , 'c' , 0 };char a[] = {'a' , 'b' , 'c' , '\0' }; char a[10 ] = {'a' , 'b' , 'c' }; char a[] = "abc" ;
2.字符串越界 定义字符串时,初始化内容的长度大于定义的字符串,就会越界。
3.字符串初始化原理 "abcdef"
储存在文字常量区,a[]
内存空间在栈区,初始化时,将文字常量区的内容 "abcdef"
,拷贝到栈区 的 a[]
。
012.字符串处理函数 1.gets() 可以读取空格,不推荐使用。
2.fgets() 三个要点:
1.写入会覆盖字符串。
2.stdin 内容 < size,写入时自动增加一个换行符。
3.当 stdin 什么都没有时,由于stdin 内容 < size,写入时自动增加一个换行符。
读取回车、空格,推荐使用。
读取遇到换行符,结束本次读取。
1 2 3 4 5 6 char *fgets (char *s, int size, FILE *stream) ;char a[100 ] = {"原来的文字将会被覆盖掉" };
3.fputs() 1 2 3 4 5 char a[] = "abc" fputs (a, stdout ); FILE *fp = fopen("1.txt" , "w" );fputs ("abc" , fp);
4.strlen() 计算字符串长度(不算结束符 “\0”)(“\n” 算一个字符)。
1 2 3 #include <string.h> char str[] = "123456" ;int len = strlen (str);
注意事项:
strlen()
:从首元素开始,到结束符为止的长度(不计算结束符)。
sizeof()
:计算数据类型的长度,不会因为结束符停止。
1 2 3 char a[100 ] = "abc" ;strlen (a); sizeof (a);
5.strcpy 拷贝字符串,自动加’\0’。
**重要:**遇到 ‘\0’ 停止拷贝。
1 2 3 char old[] = "abcdef\0gjk" ;char new[100 ];strcpy (new, old);
6.strncpy 拷贝指定n个字符,不自动加’\0’。
**重要:**遇到 ‘\0’ 停止拷贝。
1 2 3 4 5 6 7 8 9 char old[] = "abcdef\0gjk" ;char new01[100 ];char new02[100 ];int n = 3 ;strncpy (new01, old, n);
7.strcmp 判断字符串是否相同。
比较字符串大小(一个字母字母比较)。 注意:strcmp 传入参数时需保证参数不为 NULL/nullptr,因为 strcmp 实现中会直接取*,若是 NULL/nullptr,会报段错误。
1 2 3 4 5 6 7 8 9 char str1 = "abc" ;char str2 = "bac" ;if (strcmp (str1, str2) > 0 ) printf ("str1 > str2" );
附:字符串比较问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 char *str1 = "abc" ;char *str2 = "abc" ;printf (str1 == str2) char str3[] = "abc" ;char str4[] = "abc" ;printf (str1 == str2) string str5 = "abc" ;string str6 = "abc" ;printf (str1 == str2)
8.strncmp 判断指定n个字母的大小。 注意:与 strcmp 一样,传入参数时需保证参数不为 NULL/nullptr,因为 strncmp 实现中会直接取*,若是 NULL/nullptr,会报段错误。
1 2 3 4 char str1 = "abc" ;char str2 = "bac" ;if (strncmp (str1, str2,2 ) == 0 ) printf ("str1 和 str2的前两个字母 相同" );
9.strcat 将 str2 追加在 str1 后面。
1 2 3 char str1 = "abc" ;char str2 = "def" ;strcat (str1, str2);
存疑:
1 2 3 4 5 6 7 8 9 10 #include <iostream> using namespace std;int main () { char a[] = "abcaaaaa11111111111111121sdfhaljashdflhfsdajlshdlfjsdhfljhldfjashfldajsdfhljksdfhljksfdhljksfdahljkfdhljkdfhljkfdhjklshfjklsfhlkjfsadhjklafdhjklfadhjkldfaljhkfadjklfsadjlsfahdjlhfjldahdfjlsaldhfsjalhjkdlfhdsaahlfsjdahsldjsfldhjalhfjdhljfdaahsjfdhljdsdflhklsdfkajhjfsdlahfjsdhfsdjkalhdfksljfdhjkasfhjklhfsajlfsahjkfdhskjafdskhjlfdshkldfhsklhlsfhfsdlshdflkshdfhskdhfhfdkjshsafhdlklasfdhk323342egwyrgyasdhljsdfhlkashfklsdhfkjsdfklhasadlhsdjlfhalshjdsljahkdfaljkdflhjkasfhkjlsafdkljlfsahjkfsahlkfsahlkdafldhjshfladhslfjkadhjfdshjflsfhajldhjlfhfjlhlflhkjlhksaklhsfhdklsdhfjklsaldfhasdjhlfsjldhkfjhkdljkhdfaljkhadjkfdhalkfadshlsdafjklsfdkjlsfkjlsjhkflddfjkhlhjfdklfdajkhlfjkhlhjklsfdhjsfklajksfldajkhlfkhsfhkfshjkfslhfjsdlewgfy83hfuehfjkashfjhsdfhgwieuhfuishflsdf" ; char b[] = "1234567890" ; strcat (a, b); cout << a; }
但是若是定义a 的空间大一些,就不会错。
1 2 3 4 5 6 7 8 9 #include <iostream> using namespace std;int main () { char a[1024 ] = "abcaaaaa11111111111111121sdfhaljashdflhfsdajlshdlfjsdhfljhldfjashfldajsdfhljksdfhljksfdhljksfdahljkfdhljkdfhljkfdhjklshfjklsfhlkjfsadhjklafdhjklfadhjkldfaljhkfadjklfsadjlsfahdjlhfjldahdfjlsaldhfsjalhjkdlfhdsaahlfsjdahsldjsfldhjalhfjdhljfdaahsjfdhljdsdflhklsdfkajhjfsdlahfjsdhfsdjkalhdfksljfdhjkasfhjklhfsajlfsahjkfdhskjafdskhjlfdshkldfhsklhlsfhfsdlshdflkshdfhskdhfhfdkjshsafhdlklasfdhk323342egwyrgyasdhljsdfhlkashfklsdhfkjsdfklhasadlhsdjlfhalshjdsljahkdfaljkdflhjkasfhkjlsafdkljlfsahjkfsahlkfsahlkdafldhjshfladhslfjkadhjfdshjflsfhajldhjlfhfjlhlflhkjlhksaklhsfhdklsdhfjklsaldfhasdjhlfsjldhkfjhkdljkhdfaljkhadjkfdhalkfadshlsdafjklsfdkjlsfkjlsjhkflddfjkhlhjfdklfdajkhlfjkhlhjklsfdhjsfklajksfldajkhlfkhsfhkfshjkfslhfjsdlewgfy83hfuehfjkashfjhsdfhgwieuhfuishflsdf" ; char b[] = "1234567890" ; strcat (a, b); cout << a; }
10.strncat 将 指定长度的 str2 追加在 str1 后面。
1 2 3 char str1 = "abc" ;char str2 = "def" ;strcat (str1, str2, strlen ("cd" ));
11.sprintf 格式化一个字符串,并输出到指定数组中。
1 2 3 4 5 int a = 10 ;char str[] = "abc" ;char dst[100 ];sprintf (dst, "a = %d, str = %s" , a, str);printf ("%s" , dst);
12.sscanf 从字符串数组中按指定格提取内容(便于提取数字)。
(1)提取数字 1 2 3 4 char str[] = "a = 1, b = 2, c = 3" ;int a, b, c;sscanf (str, "a = %d, b = %d, c = %d" , &a, &b, &c);printf ("%d %d %d" , a, b, c);
(2)提取字符 1 2 3 4 char str[] = "hello world my friend" char m[10 ], n[10 ], k[10 ];sscanf (str, "%s %s %s" , m, n, k); pritnf("%s %s %s" , m, n, k);
13.strchr 查询字符。
1 2 3 4 5 6 7 8 9 10 char buf[] = "abcdef" ;char *p = strchr (buf, 'd' ); if (p == NULL ) { printf ("查询失败" ); } else { printf ("%s" , p); printf ("%c" , p[0 ]); }
14.strstr (1)查询字符串 1 2 3 4 5 6 7 8 9 char buf[] = "abcdef" ;char *p = strstr (buf, "abc" ); if (p == NULL ) { printf ("查询失败" ); } else { printf ("%s" , p); }
(2)模型|查找字符串个数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int main () { char *p = "12abc23dasdabcrweqabc" ; char *tmp = NULL ; int n = 0 ; while (1 ) { tmp = strstr (p, "abc" ); if (tmp == NULL ) { break ; } else { n ++; p = tmp + strlen ("abc" ); } } }
15.strtok 字符串分割,会更改原字符串,记得备份。
第一次调用,参数写原字符串地址。 第二次起调用,参数写NULL。 每次调用,返回值为切割的字符串地址。 1 2 3 4 5 6 7 8 9 char buf[] = "abc,edf,mnk,110" ;char *P = strtok(buf, "," );while (p != NULL ) { printf ("%s\n" , p); p = strtok(NULL , "," ); }
具体样例:
1 2 3 4 5 char buf[] = "abc,edf" ;char *p = strtok(buf, "," ); printf ("%s" , p); p = strtok(NULL , "," ); printf ("%s" , p);
16.atoi 将字符串转化为整型。
1 2 3 char str[] = "abc123" ;int num = atoi(str);printf ("%d" , num);
17.atof 将字符串转化为浮点型。
1 2 3 char str[] = "0.123" ;double num = atof(str);printf ("%lf" , num);
18.atol 将字符串转化为长整型。
1 2 3 char str[] = "abc123" ;long num = atoi(str);printf ("%ld" , num);
013.字符串常用模型 1.两头堵模型 有一个字符串,开头或结尾含有n个空格, (" abcdefgdddd "),欲去掉前后空格,返回一个新字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> #include <string.h> #include <stdlib.h> char *trimSpace (char *inbuf, char *outbuf) { if (inbuf == NULL ) { return NULL ; } char *begin = inbuf, *end = inbuf + strlen (inbuf) - 1 ; while (begin < end) { if (*begin == ' ' ) { begin++; end--; } else { break ; } } outbuf = (char *)malloc (end - begin + 2 ); strncpy (outbuf, begin, end - begin + 1 ); return outbuf; }int main () { char *inbuf = "" ; char *outbuf = NULL ; outbuf = trimSpace(inbuf, outbuf); printf ("%s" , outbuf); return 0 ; }
2.字符串反转模型 例如:“abcd” 变为 “dcba”。
(1)传统解法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 char *inverse (char *str) { if (str == NULL ) { return NULL ; } int lens = (int )strlen (str); char *head = NULL , *end = NULL ; head = str; end = str + lens - 1 ; while (head < end) { char tmp = *head; *head = *end; *end = tmp; head ++; end --; } return str; }
(2)递归解法 非常巧妙,strncat(buf, str, 1);
一定要放在 inverse(str+1);
后面,这样才能先写入后面的数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 char g_buf[100 ] = {0 };int inverse (char *str) { if (str == NULL ) { return -1 ; } if (str[0 ] == '\0' ) { return 0 ; } inverse(str+1 ); strncat (g_buf, str, 1 ); return 0 ; }
014.分文件编程 1.文件功能 main.c 调用自己的头文件。
1 2 3 4 5 #include "my.h" int main () { }
my.h 放置函数声明,避免每调用一次,都要写无数条函数声明。
1 2 int my_fun01 (int a) ;int my_fun02 (int a) ;
my_fun.c 写函数的具体实现。
1 2 3 4 5 6 7 int my_fun01 (int a) { }int my_fun02 (int a) { }
2.防止头文件重复包含 015.指针 1.野指针 野指针:保存非法地址的指针。
非法地址:只有定义后的变量的地址才是合法地址。
1 2 3 int *p; p = 0x1234 ; *p = 1 ;
2.空指针 1 2 3 int *p1;int *p2 = NULL ;
3.指针大小 32位编辑器 用32位(4字节)存指针。
64位编辑器 用64位(8字节)存指针。
4.多级指针 1 2 3 4 int a = 10 ;int *p1 = &a;int **p2 = &p1;int ***p3 = &p2;
5.*p 等价于 p[0] 1 2 int a = 10 ;int *p = &a;
变量a 大小为4个字节,由四个地址储存。p 是指向 a 的首地址,等价于 p[0]。
**PS:**p[1] 等价于 *(p + 1),即p加上4字节,野指针不能用。
1 2 3 int a[5 ] = {0 };int *p = a;
6.指针步长 p+1,不是 p中所指向的地址
+ 1,而是 p中所指向的地址
+ sizeof(数据类型)
。
7.万能指针 void * 定义的指针。
1 2 3 4 5 void *p; int a = 10 ; p = &a; *(int *)p = 11 ;
8.const修饰指针 (1)const * 表示指针所指向的变量只读。
const 修饰 *
1 2 3 4 int a = 10 ;const int *p1 = &a;int const *p2 = &a; p = NULL ;
(2)* const p 表示指针变量只读。
const 修饰 p
1 2 3 4 int a = 10 ;int * const p = &a; *p = 100 ; p = NULL
(3)双const
指针和指针所指变量的值都不能修改。
1 2 int a = 10 ;const int * const p = &a;
9.形参中的数组退化为指针 形参中的 a[]
是指针,写 int *a
int a[100]
int a[]
是一样的。
1 2 3 4 5 6 7 8 9 int fun (int a[]) { int n = sizeof (a)/sizeof (a[0 ]) }int main() { int a[10 ] = {0 }; fun(a); }
PS:补充见 015.13.(2) 形参中不能用 char **a
代替 char a[][10]
10.字符串常量地址相同 字符串常量 放在 data 区,相同的常量,地址相同。 【每个字符串都是一个地址 】,这个地址是字符串首地址。 1 2 3 4 5 6 7 8 9 10 11 12 int fun () { printf ("%p" , "hello" ); }int main () { printf ("%p" , "hello" ); fun(); printf ("%c" , *("hello" )); }int *p = "hello" ;
11.数组名 (1)数组名是常量 数组只有定义的时候可以初始化!
1 2 3 4 5 6 7 8 9 int a[10 ]; a = {10 }; ---------------------------------------------------char a[10 ]; a = "abc" ; strcpy (a, "abc" );
(2)数组名b
、&b
的数据类型不一样 b
表示首元素的地址。b + 1
表示 首元素的地址加 4。
&b
表示整个数组的首地址。&b + 1
表示 整个数组的首地址加 4 * 10。
(3)二维数组数组名 1 int a[3 ][4 ] = {{0 }, {1 }, {3 }};
a
、&a[0]
表示 第 0 行首地址。步长:sizeof(int [4])
a + i
、&a[i]
表示 第 i 行首地址。 步长:sizeof(int [4])
*(a + i)
、 a[i]
表示 第 i 行首元素地址。步长:sizeof(int)
*(a + i) + j
、 &a[i][j]
表示 第 i 行第 j 列元素的地址。步长:sizeof(int)
*(*(a + i) + j)
、 a[i][j]
表示 第 i 行第 j 列元素的值。
&a
表示 整个数组的首地址,&a + 1
表示 整个数组的首地址加 整个数组的大小。步长:sizeof(int [4] * 3)
【辨析 】:
&a
:整个数组的首地址
a
、a + 0
:第 0 行首地址,&a[0]
、&(*a)
*a
、*(a + 0)
:第 0 行 第 0 个元素首地址,a[0]
**a
、*(*(a + 0) + 0)
:第 0 行 第 0 个元素的值,*a[0]
、a[0][0]
(4)二维数组的本质 二维数组的本质:一维数组;也可以看成若干个一维数组的组合。
将二维数组的每一行看作一个数组,有助于理解 数组指针。
另外,由于二维数组其实也是线性存储的,所以二维数组可以当一维数组输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 void fun (int *p, int n) { for (int i = 0 ; i < n; i ++) { printf ("%d\n" , p[i]); } return ; }int main () { int a[4 ][3 ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 }; int a_size = sizeof (a) / sizeof (a[0 ][0 ]); fun((int *)a, a_size); return 0 ; }
(5)二维数组名的本质 二维数组名的本质:数组指针。
通过 + i
来指向若干个数组的首地址,a
表示第 0 行的首地址,a + 1
表示第 1 行的首地址。
(6)二维数组求行数、列数 1 2 3 int a[3 ][4 ] = {0 }; line = sizeof (a) / sizeof (a[0 ]); row = sizeof (a[0 ]) / sizeof (a[0 ][0 ]);
12.指针数组 1 2 char *p[] = {"11111" , "22222" , "22333" };
13.数组指针 (1)储存整个数组首地址 【一维数组 】数组指针指向 一维数组 整个数组的首地址,&a
,步长为整个数组的大小。
【二维数组 】每一行是一个数组,数组指针指向 一维数组 整个数组的首地址(即每一行的首地址),a
,表示第 0 行数组的首地址,步长为整个第 0 行数组的大小。
【注意 】数组指针 p 的步长,是数据类型的大小,也就是整个数组的大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 typedef int A[10 ]; A *p = NULL ;typedef int (*P) [10]; P p = NULL ; int (*p)[10 ];int a[10 ] = {0 }; p = &a;for (int i = 0 ; i < 0 ; i++) { printf ("%d" , (*p)[i]); }int b[2 ][10 ] = {0 }; p = b;for (int i = 0 ; i < 2 ; i++) for (int j = 0 ; j < 3 ; j++) { printf ("%d" , p[i][j]); }
(2)形参中不能用 char **a
代替 char a[][10]
对于一维数组,函数形参中,写 char a[]
、char a[100]
、char *a
是一样的,均为 char *
类型的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void fun (char *a, int n) { for (int i = 0 ; i < n; i++) { printf ("%c\n" , a[i]); } printf ("%p\n" , a); printf ("%p\n" , a + 1 ); return ; }int main () { char a[3 ] = {'a' , 'b' , 'c' }; int n = sizeof (a) / sizeof (char ); fun(a, n); return 0 ; }
但是对于,二维数组,函数形参中不能用 char **a
代替 char a[][10]
。
因为 a[1]
表示 *(a + 1)
,而 a 的步长是 sizeof(char *) == 8
。我们想要的步长是 10
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void fun (char **a, int n) { for (int i = 0 ; i < n; i++) { printf ("%s\n" , a[i]); } printf ("%p\n" , a); printf ("%p\n" , a + 1 ); return ; }int main () { char a[3 ][10 ] = {"aaa" , "bbb" , "ccc" }; int n = sizeof (a) / sizeof (a[0 ]); fun(a, n); return 0 ; }
(3)改进办法1:形参中规定指针步长 void fun(char **a, int n);
改为 void fun(char a[3][10], int n);
、fun(char a[][10], int n);
。
数组做形参都会退化成指针,即 a 是一个指针,所以cahr a[3]
、 char a[]
都是 char *a
的意思,指针 a 指向 数组的首地址,(每一行就是一个数组),a + i
表示 第 i 行的首地址,即第 i 行数组的首地址。
(4)改进本法2:形参中用数组指针 void fun(char **a, int n);
改为 void fun(char (*a)[10], int n);
传入的是数组的首地址,(每一行就是一个数组),即传入的是 每一行的首地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void fun (char (*a)[10 ], int n) { for (int i = 0 ; i < n; i++) { printf ("%s\n" , a[i]); } printf ("%p\n" , a); printf ("%p\n" , a + 1 ); return ; }int main () { char a[3 ][10 ] = {"aaa" , "bbb" , "ccc" }; int n = sizeof (a) / sizeof (a[0 ]); fun(a, n); return 0 ; }
14.指针函数 返回指针类型的函数。
15.函数指针 指向函数的指针。
(1)先定义函数类型,再定义指针(不常用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int fun_01 (int a) { }typedef int FUN (int a) ; FUN *p = NULL ; p = &fun_01; p = fun_01; fun(5 ); p(6 ); (*p)(6 );
(2)先定义函数指针类型,根据类型定义指针变量
1 2 3 4 5 6 7 8 typedef int (*PFUN) (int a) ; PFUN p = fun_01 p(7 );
(3)直接定义函数指针(常用)
1 2 3 4 5 6 7 8 int (*p)(int a) = fun_01; p(8 );int (*p)(int a); p = fun_01; p(9 );
16.函数指针数组 函数指针组成的数组。
int (*p_fun[5])() = {add, subtract, multiply, divide, exit};
大括号内写函数名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void add () { }void subtract () { }void multiply () { }void divide () { }void exit () { }
没使用函数指针数组之前,制作菜单,根据命令调用函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int main () { char command[100 ]; while (1 ) { printf ("inputs your command please:" ); scanf ("%s" , command); if (strcmp (command, "add" ) == 0 ) { add(); } if (strcmp (command, "subtract" ) == 0 ) { subtract(); } if (strcmp (command, "multiply" ) == 0 ) { multiply(); } if (strcmp (command, "divide" ) == 0 ) { divide(); } if (strcmp (command, "exit" ) == 0 ) { exit (); } } }
使用函数指针之后。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main () { char command[100 ]; int (*p_fun[5 ])() = {add, subtract, multiply, divide, exit }; char *command_str[5 ] = {"add" , "subtract" , "multiply" , "divide" , "exit" }; while (1 ) { scanf ("%s" , command); for (int i =0 ; i < 5 ; i++) { if (strcmp (command, *command_str[i]) == 0 ) { p_fun[i](); break ; } } } }
17.回调函数 在函数中调用函数,通过传递不同的函数指针,实现同一个函数框架,调用不同的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 int add (int a, int b) { return a + b; }int subtract (int a, int b) { return a - b; }int multiply (int a, int b) { return a * b; }int divide (int a, int b) { return a / b; }int fun (int x, int y, int (*p_fun)(int a, int b) ) { return p_fun(x, y); }int main () { int a = 1 , b = 1 ; int res = fun(a, b, add); }
18.指针易错 (1)指针值传递 调用函数修改指针的值,需要传递的参数是 指针的地址,并用二级指针的来做形参,接收指针的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int a = 1 , b = 2 ; void fun (int *p) { p = &b; }int main () { int *p = &a; fun(p); }int a = 1 , b = 2 ;void fun (int **p) { *p = &b; }int main () { int *p = &a; fun(&p); }
(2)二级指针没有空间 一级指针没有指向内存空间,通过操作二级指针给一级指针所指内存拷贝内容。【内存污染】
1 2 3 4 char **pp = (char **)malloc (3 * sizeof (char *)); char ch[] = "abcd" ;strcpy (pp[0 ], ch);
016.main()函数参数 1 2 3 int main (int argc, char *argv[]) int main (int argc, char **argv)
argc
储存的是 argv[]
的 数组大小
argv[]
存储的是命令 + 参数,举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void main (int argc, char *argv[]) { printf ("argv[0] = %s\n" , argv[0 ]); printf ("argv[1] = %s\n" , argv[1 ]); }
017.内存管理 1.普通局部变量 (1)作用域:括号
(2)离开{},内存自动释放
(3)就近原则
1 2 3 4 5 6 7 int main () { int a = 10 ; { int a = 11 ; printf ("%d" , a); } }
2.static局部变量 (1)作用域:括号
(2)离开{},不会释放;程序结束,static局部变量才自动释放【存储在data区 ,程序不结束,data区数据不释放】。
1 2 3 4 5 6 7 8 9 10 11 int fun () { static int a; a ++; printf ("%d" , a); }int main () { fun(); fun(); fun(); }
(3)data区数据,在编译阶段已分配空间,程序还没执行,static 局部变量就存在。
(4)static 局部变量不初始,值默认 = 0。多次调用初始化语句,只会执行一次。
(5)static 局部变量只能用常量初始化。
1 2 int a = 10 ;static b = a;
3.普通和static局部变量的区别 (1)普通局部变量只有执行到定义变量的语句 才分配空间;static局部变量编译时就分配空间。
(2)普通局部变量离开{} ,自动释放;static局部变量程序结束,自动释放。
(3)普通局部变量不初始化,值为随机数,static局部变量不初始化,值为0【初始化语句只执行一次】。
4.全局变量 (1)使用变量时,前面没有变量定义,需声明。
1 2 3 4 5 6 7 8 9 int fun () { puts (a); }int a; int main () { fun(); }
(2)分文件写,在main.c中定义,在头文件中声明。
为什么在头文件中声明? 避免函数需要进行多次声明。【见 014.分文件编程 】**为什么在main.c中定义?**不能在头文件中定义,否则多次调用头文件时,会出现多次定义全局变量的问题。 5.static全局变量 static全局变量,只能在本文件中使用,不能在其他文件使用。 普通全局变量可以在所有文件中使用。 6.内存分区 (1)内存四区 堆区 heap
栈区 stack
全局区
代码区 code
(2)内存加载顺序 (3)栈区堆区扩展方向 栈区:从高地址向低地址扩展。
堆区:从低地址向高地址扩展
7.内存操作函数|memset void 是万能指针,可以接收任何类型指针【见015.7.万能指针 】,size_t 是无符号整型。
【特别注意 】
memset(b, 0, sizeof(b))
是对 每个字节 都赋值为 0;以64位 int 为例,四个字节分别赋值为 0(最后该数字还是等于0)。
【进一步探究 】
对于 char 而言 ,可以赋值任何字符,因为其只有一个字节。
对于 int 而言 ,除了 0 之外,还可赋值为 -1、0x3f3f3f。
当赋值为 -1,-1 的反码是 1111 1111 1111 1111 1111 1111 1111 1111
,存入 1 字节就是 1111 1111
,每个字节都是 1111 1111
,那四个字节就是 1111 1111 1111 1111 1111 1111 1111 1111
,所以该 int 数字还是 -1。 当赋值为 0x3f3f3f,其反码是 0x3f3f3f,存入 1 字节就是 0x3f,每个字节都是 0x3f,那四个字节就是 0x3f3f3f,所以该 int 数字还是 0x3f3f3f。 1 2 3 4 5 6 7 8 9 10 11 12 13 void *memset (void *s, int c, size_t n) ;int a;memset (&a, 0 , sizeof (a));printf ("%d" , a); memset (&a, 97 , sizeof (a));printf ("%c" , a) int b[10 ];memset (b, 0 , sizeof (b));
8.内存操作函数|memcpy 1 2 3 4 5 6 7 8 void *memcpy (void *dest, const void *src, size_t n) ;char p[] = "hello\0world" ;char buf[100 ];memcpy (buf, p, sizeof (p));
9.内存操作函数|memmove **用途:**将 a[0], a[1], a[2] 所存内容移动到 a[2], a[3], a[4].(如果用 memcpy 拷贝,会发生内存重叠)。
1 2 3 4 int a[5 ] = {1 , 2 , 3 }; memmove(&a[2 ], a, 3 * sizeof (int ));
10.内存操作函数|memcmp 与 strncmp 相似,**用途:**判断是否相等。
1 2 3 4 5 6 7 int a[10 ] = {1 , 2 , 9 };int b[10 ] = {9 , 2 , 1 };int flag = memcmp (a, b, 10 * sizeof (int ));
1 2 3 4 5 6 7 struct MyStruct { char a; int b; }; MyStruct A, B; memcmp (&A, &B, sizeof (MyStruct));
11.内存释放 内存释放只能释放一次,释放多次会发生段错误。
内存泄漏: 动态分配空间,不释放空间。
内存污染: 非法使用内存(操作野指针所指向的内存、堆区越界)
1 2 3 4 5 6 int *p = NULL ; p = (int *) malloc (sizeof (int )); if (p != NULL ) { free (p); p = NULL ; }
补充:
第五行,null 代表着,该指针被释放;未被赋值为 null,代表着指针所指空间未被释放。 未被赋值为 null 的指针,容易造成二次释放。已释放的空间,二次释放,将出现错误。 12.堆区空间越界 malloc
分配的空间 < 操作的空间,会发生内存污染。
1 2 char *p = (char *) malloc (1 );strcpy (p, "123abc" );
13.堆区数组 1 2 3 4 int *p = NULL ; p = (int *)malloc (10 * sizeof (int )); p[0 ] = 1 ; p[1 ] = 2 ;
14.内存污染|返回栈区地址 (1)问题 函数执行完,自动释放栈区内存空间,(可以返回结果,不能返回空间地址 )。
错误案例01:
1 2 3 4 5 6 7 8 9 int *fun () { int a = 10 ; return &a; }int main () { int *p = fun(); *p = 20 ; }
错误案例02:
1 2 3 4 5 6 7 8 9 10 char *get_char () { char str[] = "abcdef" ; return str; }int main () { char buf[100 ] = {0 }; strcpy (buf, get_str()); printf ("%s" , buf); }
(2)改进 将函数体内局部变量定义为 static ,储存在 data 区,程序结束前,不会释放。
1 2 3 4 5 6 7 8 9 int *fun () { static int a = 10 ; return &a; }int main () { int *p = fun(); *p = 20 ; }
15.内存泄漏|值传递 (1)问题 传递的是指针的值(NULL),函数中申请堆区空间,tmp 指向堆区,但是 p 并没有指向堆区。
1 2 3 4 5 6 7 8 9 10 void fun (int *tmp) { tmp = (int *)malloc (sizeof (int )); *tmp = 10 ; }int main () { int *p = NULL : fun(p); printf ("%d" , *p); }
(2)改进|返回堆区空间 1 2 3 4 5 6 7 8 9 10 11 int *fun (int *tmp) { tmp = (int *)malloc (sizeof (int )); * tmp = 10 ; return tmp; }int main () { int *p = NULL ; p = fun(p); printf ("%d" , *p); }
16.非法使用内存导致错误 非法使用指针,造成内存污染。例如:
17.如何避免非法使用内存 定义一个指针后,先指向一块内存再使用!
指向栈区内存(即指向普通变量) 1 2 3 char *p;char s[] = "ab" ; p = s;
指向堆区(malloc申请空间) 【堆区空间 记得 free()】
1 2 char *p; p = (char *)malloc (sizeof (char ) * 10 );
3.指向文字常量区
018.结构体 复合类型
1.格式 1 2 3 4 5 6 struct Student { int age; char name[64 ]; };
和数组一样:结构体变量只有定义的时候可以初始化。 初始化使用大括号,而且有分号。struct student s1 = {18, "Tom"};
【易错 】没初始化,不能操作 字符数组名(line 4) 1 2 3 4 5 6 int main () { struct Student s1 ; s1.age = 18 ; s1.name = "tom" ; strcpy (s1.name, "tom" ); }
2.不常用定义方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct Student { int age; char name[64 ]; }s1, s2; ------------------------------------- struct Student { int age; char name[64 ]; }s1 = {18 , "Tom" }; -------------------------------------struct { int age; char name[64 ]; }s3, s4;
3.结构体数组初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct Student { int age; char name[64 ]; }; struct Student s [5] { {18 , "student_01" }, {19 , "student_02" }, {20 , "student_03" } };struct Student double_s [5] { 18 , "student_01" , 19 , "student_02" , 20 , "student_03" };
4.结构体嵌套 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct Score { int chinese; int math; }struct Student { int age; char name[64 ]; struct Score score ; }struct Student s = {90 , 100 , 18 , "tom" } s.score.math = 110 ;
5.结构体的值传递和地址传递 值传递,是直接内存拷贝,效率不高;地址传递效率更高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct Student { int age; char name[64 ]; }; void fun (struct Student *p ) { p->age = 10 ; memcpy (p->name, "Tom" , sizeof ("Tom" )); }int main () { struct Student s1; fun (&s1); } ---------------------------------------------------void fun (const struct Student *p) {}
6.结构体指针套一级指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct Student { int age; char *name; }; int main () { struct Student s1 ; s1.age = 18 ; s1.name = "Tom" ; printf ("%s" , s1.name); struct Student *p ; p = (struct Student *)malloc (sizeof (struct Student)); p->age = 18 ; strcpy (p->name, "tom" ); p->name = "Tom" ; }
7.结构体指针数组套二级指针 用一个指针指向 导师结构体数组,表示有 teacher_num 个导师,一个导师有 student_num 个学生。用二级指针指向学生数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 typedef struct Teacher { char **student; }Teacher; Teacher *creatTeacher (int teacher_num) { Teacher *p = (Teacher *)malloc (sizeof (Teacher) * teacher_num); return p; }void creatStudent (Teacher **p, int teacher_num, int student_num) { for (int i = 0 ; i < teacher_num; i++) { (*p)[i].student = (char **)malloc (sizeof (char *) * student_num); for (int j = 0 ; j < student_num; j++) { (*p)[i].student[j] = (char *)malloc (30 ); char tmp_name[30 ] = {0 }; sprintf (tmp_name, "s_name_%d_%d" , i, j); strcpy ((*p)[i].student[j], tmp_name); } } return ; }void printAllStudent (Teacher **p, int teacher_num, int student_num) { int n = 0 ; for (int i = 0 ; i < teacher_num; i++) { for (int j = 0 ; j < student_num; j++) { printf ("第%d名,%s\n" , n++ ,(*p)[i].student[j]); } } return ; }int freeAllSpace (Teacher **p, int teacher_num, int student_num) { for (int i = 0 ; i < teacher_num; i++) { for (int j = 0 ; j < student_num; j++) { if ((*p)[i].student[j] != NULL ) { free ((*p)[i].student[j]); (*p)[i].student[j] = NULL ; } } if ((*p)[i].student != NULL ) { free ((*p)[i].student); (*p)[i].student = NULL ; } } if (*p != NULL ) { free (*p); *p = NULL ; } return 0 ; }int main () { Teacher *p = NULL ; int teacher_num = 3 ; p = creatTeacher(teacher_num); int student_num = 2 ; creatStudent(&p, teacher_num, student_num); printAllStudent(&p, teacher_num, student_num); freeAllSpace(&p, teacher_num, student_num); }
8.结构体的深拷贝与浅拷贝 浅拷贝 (Shallow Copy):结构体中嵌套指针,而且动态分配空间,同类型结构体变量赋值,不同结构体成员指针变量指向同一块内存。深拷贝 (Deep Copy):人为申请空间,重新拷贝堆区内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> int main () { typedef struct Teacher { char *course; int score; }Teacher; Teacher t1; t1.course = (char *)malloc (10 ); t1.score = 90 ; strcpy (t1.course, "math" ); Teacher t2 = t1; }
9.结构体内存 由于结构体存在字节对齐,所以结构体变量的内存大小,大于其成员变量的内存大小之和。
**注意:**当结构体变量更改顺序时,所占内存大小不一样。
1 2 3 4 5 6 7 8 9 10 11 struct tmp1 { int a; short b; char c; };struct tmp2 { short b; int a; char c; };
(1)结构体偏移量 结构体类型定义下来,内部的成员变量的内存布局已经确定下来。
(2)结构体对齐规则 1 2 3 4 5 6 struct A { char c; double d; short s; int i; };
系统默认以最长字节的类型的大小来对齐:8 个字节 。 变量 偏移量 sizeof(类型) * 倍数 所占位置 首地址 char c 1 * 0 = 0 1 1 double d 8 * 1 = 8 9 - 16 9 short s 2 * 8 = 16 17 - 18 17 int i 4 * 5 = 20 21 - 24 21
字节对齐可以程序控制,采用指令 。但对齐参数不能大于最长字节的类型大小。1 2 3 4 5 6 #pragma pack(xx) #pragma pack(1) #pragma pack(2) #pragma pack(4) #pragma pack(8) #pragma pack(16)
当系统以 4 个字节对齐 。当最长类型大小 > 对齐参数,按照对齐参数计算偏移量。
变量 偏移量 sizeof(类型) * 倍数 所占位置 首地址 char c 1 * 0 = 0 1 1 double d 4 * 1 = 4【最长类型大小 > 对齐参数】 5 - 12 5 short s 2 * 7 = 14 15 - 16 15 int i 4 * 4 = 16 17 - 20 17
当系统以 1 个字节对齐 。
(3)特殊情况|当结构体成员为数组时 当结构体成员为数组时,并不是将整个数组当成一个成员来对待,而是 将数组的每个元素当一个成员来分配,其他分配规则不变。比如 int a[5]
,当作 5 个 int
类型来处理。
(4)特殊情况|当有不完整类型时 1 2 3 4 5 6 7 struct A { int a1:5 ; int a2:9 ; char c; int b:4 ; short s; };
【要点】:
对于位段成员,依旧按类型分配空间。 同类型的、相邻的位段成员,可当作一个类型变量来处理。比如 a1:5
a2:9
可以当作一个 int
变量处理。 系统默认以 4 个字节来对齐:
a1:5
表示 占 5bit,a2:9
表示 占 9bit,加起来一共 16bit,未超过 int
的 4 个字节。所以将 a1:5
a2:9
放在一个 int
空间。由于 b:4
和 s
的类型不一样,所以不能放在一起,b:4
依旧占 4 个字节。
10.typedef 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct Student { int age; char *name; }Student;typedef int int64;typedef int A[8 ]; A a; typedef int (MY_FUNC) (int , int ) ;
11.比较结构体是否相等 不可使用 memcmp 判断,因为用于内存对齐的几个字节是垃圾值。
使用重载 == 操作符。
019.共用体(联合体)、枚举 1.共用体 1 2 3 4 5 6 7 8 union Test { unsigned char a; unsigned short b; unsigned int c; };union Test obj ;
2.枚举 1 2 3 enum { red, write, blue };
020.文件操作 1.缓冲区 ANSI C标准采用“缓冲文件系统”处理数据文件。
(1)为什么要有缓冲区? 提高交互效率
fputs()
把内存的值写入文件:内存 -> 缓冲区 -> 缓冲区(缓冲区满了,或者程序结束) -> 屏幕(标准输出文件)。
fgets()
从文件读取数据:键盘(标准输入文件) -> 缓冲区 -> 缓冲区(缓冲区满了,或者程序结束)-> 内存。
(2)缓冲区写入文件的条件 【标准设备文件】stdin
stdout
stderr
可以实时读写。
【缓冲区满了】但是缓冲区不同系统大小不一样,缓冲区相当于一个固定大小的字符串。
【程序结束】程序正常关闭,缓冲区的内容,自动写入文件。
【关闭文件】fclose(fp)
文件正常关闭,缓冲区的内容,自动写入文件。
【手动刷新缓冲区】fflush(fp)
仅限 Linux。文件不关闭,程序没结束,实时刷新缓冲区。
(3)刷新缓冲区|fflush() 1 int fflush (FILE *stream) ;
【linux系统下,刷新缓冲区】:当缓冲区积累的内容没有足够多,或者程序未结束,缓冲区的内容还未写入文件,需要手动刷新一下缓区。
1 2 3 4 5 6 7 8 char buf[] = "this is test" ; FILE *fp = fopen("./test.txt" , "w+" );fputs (buf, fp); fflush(fp); getchar(); fclose(fp);
2.文件指针 fp指针 是结构体指针,结构体内部是,若干个数据成员,保存文件状态等各种信息(不必关心内部)。 fp指针,调用 fopen()
时 ,在堆区分配空间,地址返回给 fp,【故 fp指针 不指向文件,而是指向堆区的结构体,该结构体用来储存文件状态等信息】。 通过文件库函数操作 fp指针,来修改该结构体内部数据成员。 1 2 3 4 5 6 7 8 9 10 11 12 typedef struct { short level; unsigned flags; char fd; unsigned char hold; short bsize; unsigned char *buffer; unsigned ar; unsigned istemp; short token; }FILE;
3.文件分类 (1)分类 (2)win 和 linux 文本文件的区别 win 文本文件的换行符是 “\r\n” linux 文本文件的换行符是 “\n” ps:所以记事本打开会不换行。
win 下读写 会自动转换换行符。
在 win 下,读取文本文件时,系统将 “\r\n” 转化为 “\n” 写入文件时,系统将 “\n” 转化为 “\r\n” 4.文件操作流程 打开文件fopen() 读写文件按 字符 读写 fgetc()、fputc() 按 字符串(行)读取文件 fgets()、fputs() 文件结尾判断 feof() 【end of file】 关闭文件 fclose() 5.标准设备文件指针 stdin:标准输入 stdout:标准输出 stderr:标准出错,perror
函数输出错误信息 补充:
fclose() 关闭文件 perror() 打印库函数调用失败原因失败:Bad file descriptor 成功:Sucess 或者 不显示信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main () { printf ("aa\n" ); fclose(stdout ); printf ("bb\n" ); perror("my error" ); int a = 0 ; fclose(stdin ); scanf ("%d" , &a); perror("my new error" ); }
6.文件打开和关闭 (1)fopen() fp指针,调用 fopen()
时 ,在堆区分配空间,地址返回给 fp。失败返回 NULL。
<1> 路径 1 2 3 4 5 6 7 8 9 10 11 12 FILE *fp = NULL ; fp = fopen("my_code\\hello\\hell.txt" , "w" ); fp = fopen("my_code/hello/hell.txt" , "w" );char *p = "1.txt" ; fp = fopen(p, "w" );
**Tip:**如果路径字符串过长,可以用续行符。
1 2 3 char *p = "1234567" \ "asdasfsdgasfdgs" ;printf ("%s" , p);
<2> 权限 w
:【写入】如果文件不存在,新建;如果文件存在,清空内容再打开。
r
:【只读】如果文件不存在,打开失败。
a
:【追加】如果文件不存在,新建;如果文件存在,光标自动放在文件最后末尾。
r+
:【读写】允许读写,文件必须存在
w+
:【读写】允许读写,文件不存在,则创建,文件存在,则清空
rb:window 平台下,r或者rt 读文本文件,rb 表示二进制文件【linux 写 rb 不影响】。
wb:window 平台下,w或者wt 写文本文件,wb 表示二进制文件【linux 写 wb 不影响】。
<3> 注意 编译的同时运行程序,相对路径不一定相对于源代码路径。 直接运行程序,相对路径是相对于可执行程序。 (2)fclose() 7.写文件|fputc()、fputs() 1 int fputc (int ch, FILE *stream) ;
将 ch 转换为 unsigned char 后,写入 stream 指定文件中。
返回值: **
流程 :写入缓冲区 -> 写入硬盘中的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FILE *fp = NULL ; fp = fopen("1.txt" , "w" );if (fp == NULL ) { perror(fopen); return -1 ; } fputc('a' , fp); fclose(fp);
PS:写文件|fputs() 见 012.3 fputs()
8.读文件|fgetc()、fgets() 1 int fgetc (FILE *stream) ;
从 stream 指定的文件中读取一个字符。
返回值:
1 2 3 4 5 6 7 8 9 10 11 fgetc(stdin ); FILE *fp = fopen("1.txt" , "r" ); char ch; ch = fget(fp);printf ("%c" , ch); ch = fget(fp); printf ("%c" , ch);
PS:读文件|fgets() 见 012.2 fgets()
当 fgets() 读到文件结尾,读取失败,保存上一次读取的值(按行读取,也就是上一行的值)。
9.判断文件结尾|feof() feof() 任何文件都能判断。
文本文件,结尾有一个隐藏的 -1,因为ascii码中没有 -1 ,EOF(end of)的宏定义为 -1。
1 2 3 4 5 6 7 8 9 10 11 FILE *fp = fopen("1.txt" , "r" );while (1 ) { char ch; ch = fget(fp); if (ch != -1 ) { break ; } printf ("%c" , ch); }
二进制文件不能用 -1 判断结尾。
任何文件都能判断是否结尾,如果到文件结尾,feof() 返回真。
1 2 3 4 5 6 7 8 9 10 11 FILE *fp = fopen("1.txt" , "r" );while (1 ) { char ch; ch = fget(fp); if (feof(fp)) { break ; } printf ("%c" , ch); }
原理: 判断的是,光标前一个字符是否是最后一个字符。
对于空文件,应该先读取,让光标后移,才能判断是否结尾。 对于文本文件,读取到 隐藏字符 -1,feof() 才能判断结尾。 1 2 3 FILE *fp = fopen("1.txt" , "r" ); feof(fp);
10.格式化|fprintf() 1 2 3 FILE *fp = fopen("1.txt" , "w" ); int num = 10 ;fprintf (fp, "num = %d\n" , &num);
11.格式化|fscanf() 1 2 3 FILE *fp = fopen("1.txt" , "r+" ); int num = 0 ;fscanf (fp, "num = %d\n" , &num);
读取失败时(读到末尾,边界值),自动保存上一次读取的值。 1 2 3 4 5 6 7 8 9 FILE *fp = fopen("1.txt" , "r+" ); int num = 0 ;fscanf (fp, "num = %d\n" , &num); fscanf (fp, "num = %d\n" , &num); fscanf (fp, "num = %d\n" , &num);
12.按块读写文件 (1)fwrite() 1 size_t fwrite (const *ptr, size_t size, size_t number, FILE *stream) ;
返回值:
成功:返回写入内容的块数目 number,(不是文件总大小)。 失败:返回 0。 参数:
ptr:准备写入文件数据的地址
size:往文件写入内容的块大小,数据类型的大小
number:往文件写入内容的块数目
stream:操作的文件指针
总大小:
size * number
是总大小,所以 size 和 number 的参数位置调换,没有影响。
1 2 3 4 char *p = "Tom" ; FILE *fp = fopen("1.txt" , "w" );int ret = fwrite(p, sizeof (char ), strlen (p), fp); fclose(fp);
(2)fread() **。返回值:**同 fwrite()
**参数:**同 fwrite()
注意:
文件内容 > 用户指定总大小 size * number
,返回值为用户指定大小的块数目。 文件内容 < 用户指定总大小 size * number
,返回值为实际读取的块数目,或者为 0(比如,number = 0.2时,返回为 0 )。 1 2 3 4 char s[100 ] = {0 }; FILE *fp = fopen("1.txt" , "r" );int ret = fread(s, sizeof (char ), 50 , fp); fclose(fp);
(3)返回值与边界问题 对于二进制文件,所读取的字符串,不要用 strlen()
判断长度。
读文件时,应该用 ret == 0
做是否读到的判断,不应该用 strlen() == 0
做是否读到的判断,因为有些特殊字符会导致出现 ‘/0’
。 1 2 3 4 5 char a[100 ] = {0 };int ret = fread(a, sizeof (char ), sizof(a), r_fp);if (ret == 0 ) { break ; }
对读到的字符串进行处理,应该用 i < ret
做循环,不应该用 i < strlen()
做循环,因为有些特殊字符会导致出现 ‘/0’
。 1 2 3 for (int i =0 ; i < ret; i++) { a[i] = a[i] + 1 ; }
写文件时,应该读多少写多少,避免越界。 1 2 3 4 5 int ret = fread(a, sizeof (char ), sizof(a), r_fp); fwrite(a, 1 , ret, w_fp);
13.光标移动|fseek() **功能:**移动文件光标
1 int fseek (FILE *stream, long offset, int whence) ;
参数:
offset:偏移量 whence:SEEK_SET:从文件开头移动 offset 个字节。 SEEK_CUR:从当前位置移动 offset 个字节。 SEEK_END:从文件末尾移动 offset 个字节。 1 2 3 4 fseek(fp, 0 , SEEK_SET); fseek(fp, 100 , SEEK_SET); fseek(fp, 0 , SEEK_END); fseek(fp, -1 , SEEK_END);
14.获取光标位置|ftell() 返回的长整型为,光标距离开头的位置。
1 long ftell (FILE *stream) ;
15.光标回到开头|rewind() 1 void rewind (FILE *stream) ;
16.应用:获取文件大小 读方式打开文件。 光标移到文件结尾。 返回文件位置。 关闭文件 或者 恢复光标 1 2 3 4 5 6 7 8 9 10 11 12 fp = fopen("test.txt" , "rb+" ); fseek(fp, 0 , SEEK_END);long size = ftell(fp); rewind(fp); fclose(fp);
021.链表 1.链表和数组的区别 链表:
数组:
2.分类 (1)动态链表和静态链表 类似于,动态数组和静态数组,储存结构的两种不同表示方式。
1 2 3 4 int *p = malloc (10 * sizeof (int ));int a[10 ];
**静态链表:**所有结点程序定义,不是临时申请的,无需释放。
**动态链表:**一个个临时申请的结点和输出的数据。建立的前后相连的关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct structure { int num; struct structure *p ; }int main () { struct structure s1 , s2 , *head ; head = &s1; s1.num = 1 ; s1.p = &s2; s2.num = 2 ; s2.p = NULL ; }
(2)带头链表和不带头链表 **带头链表:**头结点固定,储存的并不是有效数据,可能结点的个数。有效结点从第二个开始,若需在头部插入新结点,插在头结点和第二个结点之间即可。
**不带头结点:**头结点不固定,若需在头部插入新结点,新结点指针指向头结点即可。
(3)单向链表和双向链表 (4)循环链表 3.增删改查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 #include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next ; }Node; Node *ListCreat () { Node *head = NULL ; head = (Node *)malloc (sizeof (Node)); if (head == NULL ) { return NULL ; } head->data = -1 ; head->next = NULL ; Node *current = head; Node *new_ptr = NULL ; int data = 0 ; while (1 ) { puts ("input your data please:" ); scanf ("%d" , &data); if (data == -1 ) { break ; } new_ptr = (struct Node *)malloc (sizeof (struct Node)); if (new_ptr == NULL ) { continue ; } current->next = new_ptr; new_ptr->data = data; new_ptr->next = NULL ; current = new_ptr; } return head; }void ListShow (Node *head) { if (head == NULL ) { return ; } Node *current = head->next; while (current != NULL ) { printf ("%d\n" ,current->data); current = current->next; } return ; }int ListInset (Node *head, int place_data, int new_data) { if (head == NULL ) { return -1 ; } Node *aim_ptr = NULL ; Node *current = head; while (current->next != NULL ) { if (current->next->data == place_data) { aim_ptr = current; break ; } current = current->next; } Node *new_ptr = (Node *)malloc (sizeof (Node)); new_ptr->data = new_data; if (aim_ptr == NULL ) { new_ptr->next = NULL ; current->next = new_ptr; return 0 ; } else { new_ptr->next = aim_ptr->next; aim_ptr->next = new_ptr; } return 0 ; }int ListDelete (Node *head, int delete_data) { if (head == NULL ) { return -1 ; } Node *current = head; while (current->next != NULL ) { if (current->next->data == delete_data) { Node *tmp = current->next; current->next = tmp->next; tmp->next = NULL ; free (tmp); tmp = NULL ; break ; } current = current->next; } return 0 ; } Node *ListClean (Node *head) { Node *tmp = NULL ; while (head != NULL ) { tmp = head->next; free (head); head = tmp; } return head; }int main () { Node *head = NULL ; puts ("ListCreat:" ); head = ListCreat(); ListShow(head); puts ("ListInset:" ); ListInset(head, 5 , 4 ); ListShow(head); puts ("ListDelete:" ); ListDelete(head, 6 ); ListShow(head); puts ("ListClean:" ); head = ListClean(head); ListShow(head); }
4.交换节点 交换节点 = 交换结构体储存内容 + 交换指针指向。
1 2 3 4 5 6 7 8 9 tmp = *pCur; *pCur = *pPre; *pPre = tmp; tmp.next = pCur->next; pCur->next = pPre->next; pPre->next = tmp.next;
PS:如果数据域只有一个,直接交换数据域。
1 2 3 tmp = p1->data; p1->data = p2->data; p2->data = tmp;
PSS:如果数据域有多个,可以封装成一个结构体 data,在交换 data。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct Node { struct Data { int id; char name[64 ]; int num; } data; Node *next; };struct Data tmp ; tmp = p1->data; p1->data = p2->data; p2->data = tmp;
5.链表反转 先用 p_next 保存第 3 个节点地址。 在将第 2 个节点(p_cur)的指针域指向第 1 个节点(p_pre)。 p_pre、p_cur 整体后移 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int ListNodeReverse (Node *head) { if (head == NULL || head->next == NULL || head->next->next == NULL ) { return -1 ; } Node *p_pre = head->next; Node *p_cur = p_pre->next; Node *p_next = NULL ; while (p_cur != NULL ) { p_next = p_cur->next; p_cur->next = p_pre; p_pre = p_cur; p_cur = p_next; } head->next->next = NULL ; head->next = p_pre; return 0 ; }
022.常见错误 1. Segmentation Fault 段错误,主要原因是 操纵了非法内存 。
常见情景有:
1 2 head = NULL ; head->next = NULL ;
1 2 int *p = NULL ;printf ("%d" , *p);