二刷C语言后,一万字整理细碎知识点

基础知识篇

ASCII码

主要以下几点记住比较好

  • A-Z 65-90
  • a-z 97-122
  • 对应的大小写字母差值32
  • "\n"对应的ASCII是10
  • 0-31这些特殊字符不可以在标准输出上打印

sizeof表达式

用法
  1. sizeof (类型)
  2. sizeof 表达式(表达式可以不加括号)
返回结果size_t

size_t返回的是对象所占内存大小,单位是字节,不过size_t在表示的可能的是unsigned int,也可能是unsigned long,unsigned long long,具体取决于平台,这也增加了代码的可移植性,因为size_t总代表当前平台size_t返回类型

sizeof返回的表达式不计算
c 复制代码
int a = 1, b = 2;
printf("%d", sizeof(b = a + 1)); //4,int的大小
printf("%d", b); //2,上面的运算不进行

原因是在编译时就完成处理了,表达式的运算要在运行期间进行,所以自然就不计算了

signed 和 unsigned

是否有符号,用第一位二进制位表示正负,因此无符号在正数范围内表示范围大了一倍

值得注意的是,int 等价于signed int,但是chat 不等价与signed char,具体有环境确定

printf

左对齐与右对齐

printf默认右对齐,前面用空格填充,如果想要左对齐,使用'-'

c 复制代码
printf("%-5d", 2);
最小宽度和小数位数的参数传入
c 复制代码
printf("%*.*d", 5, 2, 2); //输出结果  2.00
输出部分字符串
c 复制代码
printf("%.5s\n", "hello world"); //输出结果 hello
参数个数问题

printf中占位符的个数与参数个数一一对应,有n个占位符,printf就有(n + 1)个参数,因为

前面制定的format也是参数

返回值
c 复制代码
int printf(const char* format, ...);

返回的是屏幕上打印字符的个数,比如以下程序,输出的结果是4321

c 复制代码
printf("%d", printf("%d", printf("%d", 43)));

scanf

使用方式

对于想要存入的变量,比如int char float...这些需要传址调用,而对于指针类变量,比如字符串,就不需要,因为他本身可以看作指向字符串第一个字符的指针

读取模式

用户进行输入时,会将输入内容存入缓冲区,当用户按下回车时,开始根据占位符进行解读,直到解读完毕或者出现错误

同时,scanf会自动过滤空白字符,比如换行符、制表符、回车符等等,直接从其他字符读取

c 复制代码
//input:    -13.45e12#0
int a; float b;
scanf("%d", &a);
printf("%d\n", a); //-13
scanf("%f", &b);
printf("%d\n", b); // .45e12
返回值

scanf返回成功读取到的数据的个数,全部匹配失败返回0,读取错误或者读取到文件结尾返回EOF(end of file)

%c 和 %s

这是两个比较特殊的

  • %c 不忽略空白字符,接受读取到的第一个字符,如果要强制忽略的话,采取
c 复制代码
char a;
scanf("   %c", &a);
  • %s 读取字符串时,遇到空白字符就停止,想要读取句子,就要多个%s一起使用,同时,%s
    在存储时,会在字符串末尾加上'\0';
赋值忽略符
c 复制代码
scanf("%d/%d/%d", &str);
scanf("%d-%d-%d", &str);
scanf("%d%*c%d%*c%d", &str);

这样,前两句能读取得第三句都可以读取,因为%*c表示这里读取的字符不存储

if else语句的悬空else问题:

c 复制代码
int a = 0, b = 1;
if(a == 1) // (1)
    if(b == 1) //(2)
        printf("a");
else
    printf("b");

这里else语句就是悬空的else,他会与最近的if匹配,即(2)

因此上面的语句不会输出任何内容

switch语句

  • 没有break就一直往下执行
  • 只有整形表达式可以做switch的判断依据
  • default和case的位置关系没有规定,只不过一般推荐default写在最后

rand与srand

  • rand是C语言中常用的随机数产生算法,但这是伪随机,对传入的数字("种子")进行一系列确定的运算后得到一个"随机数"
  • 因此,只要给定的种子是随机的,产生的数字也就是随机的,常用的种子有time和设备随机数(cpp)
  • time传入null时,返回当前时间与1970年1月1日0时0分0秒的差值,以秒为单位,返回结果的类型是time_t,是32位或者64位的随机数。

作用域和生命周期的概念

  • 作用域
    程序中的变量并不是一直有效,限定这个变量有效的代码区间,就是这个变量的作用域
  • 生命周期
    从申请内存到内存回收的时间区间就是一个变量的生命周期

static修饰

static修饰变量表示该变量为静态变量,存储在内存的静态区,生命周期与程序相同

static修饰,无论是变量还是函数,都只在本文件中有效,extern关键字也无法改变这一属性

函数栈帧与栈溢出

函数被调用时,需要在内存的栈区开辟一块空间来存放调用过程中局部变量,只要不返回空间就不释放,这就是函数栈帧;而如果栈空间被打满了,比如函数递归调用过深,就会出现栈溢出(stack overflow),导致程序被操作系统杀死

源码、反码、补码

对于正数来说三者相同,对于负数来说三者不同

源码按位取反后得到反码,反码+1后得到补码,在内存中存放的也是补码

原因:cpu只有加法器,没有加法器,通过补码就可以使用"加法"处理减法,避免额外的电路设计

逻辑右移和算术右移

逻辑右移符号位补0,算术右移补原符号位;

整形提升

对于char,short这些长度比int(4)小的类型来说,参与运算时,因为cpu中整形运算器一般处理长度为4,并且这也是寄存器的处理长度,所以这些类型会被提升为int的长度

提升方式:对于有符号的类型,用原符号位补全,对于无符号的类型,使用0来进行补全

变长数组的"变长"

变长数组的"变长"之的并不是长度的可变性,指的是数组的大小有变量或者是表达式所确定,因此他的大小是在运行时确定的,而且确定后不会发生改变

函数的定义与声明的关系

  • 所谓定义就是写出来这个函数需要进行怎样的操作,而声明就是告诉编译器有怎么样的一个函数
  • 如果函数的定义写在使用之前,就不需要声明;否则,需要先声明后使用,否则连接报错
c 复制代码
int add(int x, int y); //定义在使用之后,先声明后使用
int main()
{
    int a = add(1, 2);
    return 0;
}
int add(int x, int y)
{
    return x + y;
}

const 修饰指针变量

一句话概括:const 在 * 左边,指针指向的变量不可修改;const 在 * 右边,指针变量

本身不可修改

关于数组名的理解

数组名代表了数组首元素的地址,但是在sizeof和&上有区别

c 复制代码
int arr[10] = {0};
printf("%p", sizeof(arr));
printf("%p", &arr);
printf("%p", &arr + 1);
printf("%p", &arr[0]);

&加上数组名,取出的是整个数组的地址,因此进行指针+常数的运算时,跳过的是整个数组的地址

而sizeof(数组名),计算的是整个数组的大小

\]下表引用操作符,可以理解为先移动,在解引用,例如arr\[5\]可以理解为\*(arr + 5); #### 用二级指针模拟二维数组 所谓二级指针,其实就是指向指针的指针,而无论二维数组,还是以为数组,在内存中存放时, 其空间上都是连续的 ```c int arr1 = {1, 2, 3}; int arr2 = {4, 5, 6}; int arr3 = {7, 8, 9}; int arr4 = {arr1, arr2, arr3}; ``` 这里形成的arr4就是模拟的二维数组,只不过arr4每一行在空间上连续,但是行与行之间大概率是 分散的,而真正的二维数组无论行内还是行间都是连续的 **二维指针数组的传参**,这里可以引入指针数组的概念,即指向一个数组的指针 ```c int(*parr)[10]; //这就是一个数组指针,指向一个大小为10的数组 void function(int arr[2][3]); //这就是一个二维数组的传参,接受一个两行三列的指针数组 void function1(int(*arr)[3]); //这也是二维数组的传参方式,指向一个(首个)数组(二维数组的行) ``` #### 字符指针变量 ```c char str1[] = "hello world"; char str2[] = "hello world"; const char* str3 = "hello world"; const char* str4 = "hello world"; if(str1 == str2) printf("1"); if(str3 == str4) printf("2"); ``` 以上程序只会输出2,原因是c和cpp会把常量字符串存放在单独的内存区域,所以str3和str4两个 指针会指向同一块内存;但是如果用常量字符串去初始化字符数组的话,就会单独初始化一块新的内存空间,所以str1和str2代表了不同的数组首元素地址 #### 指针大练兵:手撕qsort 参考代码如下: ```c int cmp_func(void* p1, void* p2) { return (*(int*)p1 - *(int*)p2); } void swap(void* p1, void* p2, int sz) { for(int i = 0; i < sz; i++) { char tmp = *((char*)p1 + i); *((char*)p1 + i) = *((char*)p2 + i); *((char*)p2 + i) = tmp; } } void my_qsort(void* base, int count, int sz, int(*cmp_func)(void*, void*)) { for(int i = 0; i < count - 1; i++) { for(int j = 0; j < count - i - 1; j++) { if(cmp_func((char*)base + j * sz, (char*)base + (j + 1) * sz) > 0) { swap((char*)base + j * sz, (char*)base + (j + 1) * sz, sz); } } } } ``` ### 面试中常考的手撕函数 #### strlen 这个是最经典的之一,《高质量的C/C++编成指南》中林锐老师就提过自己关于strlen函数在面试时的手撕经历。。。 ```c int strlen(char* str) { if(str == NULL) { return 0; } char* begin = str; while(*str != '\0') { str++; } return str - begin; } ``` #### strcpy ```c char* strcpy(const char* src, char* des) { assert(src != nullptr && des != nullptr); char* ret = des; while((*des++ = *src++)); return ret; } ``` #### strcat ```c char* strcat(char* des, const char* src) { assert(des != NULL && src != NULL); char* ret = des; while(*des) { des++; } while((*des++ = *src++)); return ret; } ``` #### strcmp ```c int strcmp(const char* str1, const char* str2) { assert(str1 != NULL && str2 != NULL); while(*str1 == *str2) { if(*str1 == '\0') return 0; str1++; str2++; } return *str1 - *str2; } ``` #### strstr ```c char* strstr(const char* str1, const char* str2) { char* cp = (char*)str1; char* s1, *s2; if(!*str2) { return NULL; } while(*cp) { s1 = cp; s2 = (char*)str2; while(*s1 && *s2 && *s1 == *s2) { s1++; s2++; } cp++; } return NULL; } ``` #### strtok ```c char* strtok(char* str, const char* sep); //使用方式:例如下面获取ip信息的过程 int main() { char ip[] = "172.17.0.1"; for(str = strtok(ip, "."); str != NULL; str = strtok(NULL, ".")) { printf("%s", str); } return 0; } ``` #### memcpy 和 memmove 这两个很相像,但是前者对于重合内存的处理是未定义行为,后者可以处理重叠内存 **memcpy模拟实现** ```c void* memcpy(void* des, const void* src, int num) { void* ret = des; while(num--) { *((char*)des) = *((char*)src); des = (char*)des + 1; src = (char*)src + 1; } return ret; } ``` **memmove的模拟实现** ```c void* memmove(void* des, const void* src, int num) { void* ret = des; if(des <= src || (char*)des >= (char*)src + num) { while(num--) { *((char*)des) = *((char*)src); des = (char*)des + 1; src = (char*)src + 1; } } else { des = (char*)des + num - 1; src = (char*)src + num - 1; while(num--) { *((char*)des) = *((char*)src); des = (char*)des - 1; src = (char*)src - 1; } } return ret; } ``` 另外还有两个函数memset 和 memcmp ```c void* memset(void* des, int value, int num); int memcmp(const void* p1, const void* p2, int num); ``` ### 结构体相关 #### 位段 ##### 不跨平台 这是位段的性质,需要注意 ##### 写法 ```c struct A { char _a: 1; char _b: 5; int _c: 2; } ``` ##### 大小 位段空间的开辟以四个字节或者一个字节为单位 ##### 地址问题 因为位段实现的效果就是几个变量公用一个字节存储,所以会有一些变量的存储位置不是字节的起始位置,因此不能通过取地址的方式进行赋值(比如scanf) 可以采用的方式: ```c int num_a = 1; A a; a._a = num_a; ``` #### 联合体 Union,与结构体的区别是各个成员变量公用统一块空间,因此可以通过联合体来判断当前机器的大小端字节序 之的注意的是,联合体也遵循结构体的的内存对齐原则 ```c Union A { char a; int b; }; /*01 00 00 00*/ int main(){ Union A a; a.b = 1; if(a.a == 1) printf("小端"); } ``` #### 内存分配 * 内核空间:这个区域用户代码无权修改 * 栈区:在内核空间之上,主要存储局部变量,向下增长 * 内存映射段:文件映射、动态库等,向栈区和堆区伸展 * 堆区:动态内存,向上增长 * 数据段:主要存储全局变量和静态变量 * 代码段:主要存放只读常量(比如字符串)和二进制代码 #### 柔性数组 C99之后支持的,在结构体各个成员变量的最后,允许一个大小不固定的数组的存在,可以通过动态内存的方式来改变其大小 ### 输入输出函数 #### stdin和stdout printf和scanf,最常用,不必多说 #### 文件相关 ##### 文件打开关闭函数 fopen用于打开文件,返回一个FILE\* 类型的文件指针,用于对文件的读写操作,具有两个参数,一个是文件名,另一个是打开方式,常见的打开方式有,其实就是 排列组合的方式。。。 对于打开失败的文件,返回NULL | 打开方式 | 意义 | 打开方式 | 意义 | 打开方式 | 意义 | |------|----|------|------|------|-------| | w | 只写 | w+ | 读+写 | wb | 二进制写 | | r | 只读 | r+ | 读+写 | rb | 二进制读 | | a | 追加 | a+ | 读+追加 | ab | 二进制追加 | 对于关闭,使用fclose(FILE\*); ##### 写入函数 * fputc 和 fputs,用于文件的写入,使用方法为写入内容+文件指针,两者主要区别是前者是char,后者是char\* * fwrite,用于文件的二进制写入,需要传入写入内容,写入单元大小,写入单元数目,以及文件指针 * fprintf,针对所有输出流,使用方法类似于printf,不过需要先传入文件指针或者输出目标 ##### 读取函数 * fgetc 和 fputc,两者依旧是字符和字符串的区别,传入文件指针,用buffer来接收返回值 * fread,二进制读取,第一个参数是buffer,其他与fwrite相同 * fscanf,格式化输入,针对所有输入流,类似于scanf,也需要先知名目标输入流 ##### 随机读写函数 其实就是定位文件指针的位置 * fseek,指明文件指针的便宜量,需要传入文件指针、偏移量、选项 * ftell,获取文件指针位置,传入文件指针即可,返回size_t * rewind,将文件指针回其起始位置 ##### 读写情况判断 判断文件读取是否因为读到了EOF结束,采用feof(),而是否读写出错需要通过ferror #### 字符串操作函数 主要是两个格式化输入输出字符串函数: sprintf, sscanf,使用方法也基本与printf和scanf一样,只不过加上输入输出对象 ##### 函数示例 ```c #define SIZE 5 int main() { FILE* pf = fopen("log.txt", "w+"); if(pf == NULL) { return 1; } fputs("hello world", pf); fseek(pf, 5, SEEK_SET); fputs("hello world", pf); fseek(pf, 0, SEEK_END); int pos = ftell(pf); printf("ths size of the file currently is %d", pos); rewind(pf); int a; while((a = fgetc(pf)) != EOF) { putchar(a); } if(ferror(pf)) { printf("I/O fail\n"); } else if(feof(pf)) { printf("read successfully\n"); } char first_word[50]; fscanf(pf, "%s", first_word); printf("the first word is %s\n", first_word); fclose(pf); pf = fopen("arr.txt", "wb+"); if(pf == NULL) { return 1; } double nums = {1., 2., 3., 4., 5.}; fwrite(nums, sizeof(*num), SIZE, pf); double buffer[SIZE]; int read_num = fread(buffer, sizeof(*buffer), SIZE, pf); if(read_num != SIZE) { char errmsg[50]; sprintf(errmsg, "the errmsg is %s", "文件字符读取数目出错"); printf("%s", errmsg); } for(int i = 0; i < SIZE; i++) { printf("%d ", buffer[i]); } return 0; } ``` ### 宏 #### 末尾';'问题 最好不要加,有可能会导致在预处理阶段展开错误 #### # 可以将宏的一个参数替换为字面量,比如想实现一个宏完成对变量的值的打印 ```c #define PRINT(n) printf("the val of "#n" is %d", n) ``` #### ## 起到一个粘合剂的作用,把两边"粘"到一起 ```c #define MAX_FUNC(type) type type##_func(type a, type b){return a > b ? a : b;} MAC_FUNC(int) ``` 这样就完成了定义一个函数int_func,返回两个整形的较大者 #### 做题提醒 因为宏其实就是在预处理时完成文本替换,所以一定要注意操作符的优先级与结合性 ```c /* 比如下面的宏,预处理后是4 * A + B,而不是4 * (A + B) */ #define MAX A+B char* buffer = (char*)malloc(sizeof(int)*MAX); ```

相关推荐
ONExiaobaijs2 小时前
Java jdk运行库合集
java·开发语言·python
yu_anan1112 小时前
CTC Prefix Score计算
算法·机器学习
Stardep2 小时前
算法入门21——二分查找算法——山脉数组的峰顶索引
数据结构·算法·leetcode
mjhcsp2 小时前
P3145 [USACO16OPEN] Splitting the Field G(题解)
开发语言·c++·算法
空空潍2 小时前
hot100-合并区间(day14)
c++·算法·leetcode
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——力扣 675 题:为高尔夫比赛砍树
数据结构·算法·c·结构与算法
rit84324992 小时前
UVE算法提取光谱特征波长的MATLAB实现与应用
开发语言·算法·matlab
是娇娇公主~2 小时前
算法——【最大子数组和】
数据结构·c++·算法
StandbyTime2 小时前
C语言学习-菜鸟教程C经典100例-练习40
c语言