Bootstrap

C语言基础学习笔记

摘要:

  此博文是整理本人初入编程世界学习C语言的经历,零基础想学习C语言的可参考本文的学习方式,内容包含视频+笔记+思维导图总结,根据视频资源编写自己见解的学习笔记,再从笔记中提炼总结思维导图。笔记看得云里雾里是正常的,一是需要配合视频,二是因为写笔记时是想到哪里记到哪里,每个人侧重点不同,笔记仅供参考。

关键词C语言零基础学习途径笔记小甲鱼鱼C论坛

声明:本文作者原创,转载请附上文章出处与本文链接。

正文:

视频

  零基础适合观看B站小甲鱼C语言教学,内容生动有趣,能针对重点知识点进行详细讲解。每章都有课后作业,存放于鱼C论坛中,课后作业能编程尽量编,多捣鼓多敲代码,视频看一节笔记记一章。

在这里插入图片描述

跳转链接点这里~~

思维导图总结

看完视频,多敲代码,记好笔记,有一定的知识储备后,推荐制作一份思维导图,归纳总结所学,印象更深刻,温习时也更方便,制作思维导图工具推荐Xmind。有需要可评论私。

在这里插入图片描述
在这里插入图片描述

学习随笔

​  看视频,论坛课后作业的随笔,内容是自己的理解,想到哪里记哪里,有视频的难点重点,也有课后作业的答案和注重点,随意参考即可,按自己节奏走。

1~16.介绍和基础

17~18.数组

数组初始化的种种方式:

A. 将数组中所有元素统一初始化为某个值,可以这么写:

int a[10] = {0}; // 将数组中所有元素初始化为 0

B. 如果是赋予不同的值,那么用逗号分隔开即可:

 int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};

C. 还可以只给一部分元素赋值,未被赋值的元素自动初始化为 0:

// 表示为前边 6 个元素赋值,后边 4 个元素系统自动初始化为 0
int a[10] = {1, 2, 3, 4, 5, 6}; 

D. 有时候还可以偷懒,可以只给出各个元素的值,而不指定数组的长度(因为编译器会根据值的个数自动判断数组的长度):

int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};

E. C99 增加了一种新特性——指定初始化的元素。这样就可以只对数组中的某些指定元素进行初始化赋值,而未被赋值的元素自动初始化为 0:

// 编译的时候记得加上 –std=c99 选项
int a[10] = {[3] = 3, [5] = 5, [8] = [8]}; 

变长数组(VLA):

C99 标准新增变长数组(VLA,variable length array)的概念,这里的变长指的是数组的长度是在运行时才能决定,但一旦决定在数组的生命周期内就不会再变。(虽然可用malloc函数实现类似功能,但是VLA存储的区域为动态栈里)

printf("请输入字符的个数:");
scanf("%d", &n);
char a[n+1];

多维数组的地址计算:

以三维数组为基础,往后递推。
有数组A[m][n][p],计算A[i][j][k]的地址。
行优先:先把右边的填满。
偏移量为inp+j*p+k

列优先:先把左边的填满。
偏移量为knm+j*m+i

提醒:array[3][4]+5与array[3][4][5]不一样。


19.字符串处理函数

sizeof, sizelen
包含’\0’ 不包含’\0’

strcpy strncpy
拷贝字符串,不会自动追加’\0’

strcat strncat
拼接字符串,会自动追加’\0’

strcmp strncmp
比较字符串


20.二维数组

二维数组的存储,套娃模式启动

在这里插入图片描述


21.指针

32位系统,指针地址都是4个字节
64位系统,指针地址都是8个字节

避免访问未初始化的指针(野指针):

int *p;					//(错误)
int a;int *p=&a;		//(正确)//int* p = NULL;

23.指针数组和数组指针

指针数组(数组里存放指针)

//从符号优先级看
int *p1[5]={1,2,3,4,5};

数组指针(指针指向数组)

char *array[5] = {"FishC", "Five", "Star", "Good", "WoW"};
char *(*p)[5] = &array;
for (i = 0; i < 5; i++)
{
	for (j = 0; (*p)[i][j] != '\0'; j++)
	{
  		printf("%c ", (*p)[i][j]);
    }
 printf("\n");
}
//两者相同
Str[3] == *(str+3);

array 是数组第一个元素的地址,所以 array + 1 指向数组第二个元素;&array 是整个数组的地址,所以 &array + 1 指向整个数组最后的位置。

int array[10] = {0};

24.指针和二维数组
int array[2][3] = {{0, 1, 2}, {3, 4, 5}};
int (*p)[3] = array;
printf("**(p+1): %d\n", 		**(p+1));     		//3
printf("**(array+1): %d\n", 	**(array+1)); 		//3
printf("array[1][0]: %d\n", 	array[1][0]);  		//3
printf("*(*(p+1)+2): %d\n", 	*(*(p+1)+2)); 		//5
printf("*(*(array+1)+2): %d\n", *(*(array+1)+2));	//5
printf("array[1][2]: %d\n", 	array[1][2]);  		//5
//一个[ ]对应* //先解析括号**(p+1)

25.void指针和NULL指针

不要对void指针进行解引用。

字符串的特殊约定(%s,str);

//指针不清楚指向哪就指向地址0(NULL)
char *ch = NULL

大端序和小端序:
大端序:低位数据放内存地址高位,高位数据放内存地址低位。
小端序:低位数据放内存地址低位,高位数据放内存地址高位。

sizeof(void *) ;
//因为指针的尺寸是与编译器的目标平台相关的。比如目标平台是 32 位的,那么 sizeof(void*) 就是 4,如果是 64 位的,那么 sizeof(void *) 就是 8,如果是 16 位的,那么就是2。

26.指向指针的指针
//指针数组与指向指针的指针
int *p[5]={0};	
int **pl=p;	
char a[3][4]={0};
char (*p)[4]=a;
//数组指针与二维数组(系统跨度一样(一维指针不能☞二维数组))//本质上数组名代表地址

“*” and "&"是一对。


27.常量和指针

课后测试:

  • 2.A在赋值、初始化或参数传参的过程中,赋值号左边的类型应该比右边的类型限定更为严格,或至少是同样严格。
  • 3.应该将 p 先强制转换成 int * 类型
  • 4.可以。和2题一样的问题
  • 5.const int **q = &p;
  • 6.**q、*q
  • 7.const int * const * const q = &p;

判断大小端代码:

#include <stdio.h>
int main(void)
{
    int num = 0x12345678;
    unsigned char *p = (unsigned char *)&num;
    if (*p == 0x78){
        printf("您的机器采用小端字节序。\n");
    }
    else{
    	printf("您的机器采用大端字节序。\n");
    }
    printf("0x12345678 在内存中依次存放为:0x%x 0x%x 0x%x 0x%x\n", p[0], p[1], p[2], p[3]);
    return 0;
}

28.函数

课后测试:

  • 0.B-F-C-A-D-E
  • 1.少;(03)、void还return0(16)、printf(14)、多;(17)、o->0
  • 2.函数的声明是为函数的引用做准备
  • 3.不一定,把函数放在函数引用前即可
  • 4.重新定义的同名函数会覆盖标准库函数(前提是两者的声明一致,包括返回值和参数类型、个数一致)。

29.参数和指针

可变参数

实现可变参数,需要包含一个头文件叫:<stdarg.h>。

这个头文件中有三个宏和一个类型是我们需要用到的,一个类型是 va_list,三个宏,一个是 va_start,一个是 va_arg(展开宏 va_arg 会得到当前 argptr 所引用的可选参数,也会将 argptr 移动到列表中的下一个参数。宏 va_arg 的第二个参数是刚刚被读入的参数的类型) ,还有一个是 va_end(当不再需要使用参数指针时,必须调用宏 va_end。如果想使用宏 va_start 或者宏 va_copy 来重新初始化一个之前用过的参数指针,也必须先调用宏 va_end)。这里的 va就是 variable-argument(可变参数)的缩写。//还有一个宏void va_copy(va_list dest, va_list src);

// 函数add() 计算可选参数之和
// 参数:第一个强制参数指定了可选参数的数量,可选参数为double类型
// 返回值:和值,double类型

double add(int n, ...) //int myprintf(char *format, ...)
{           
    //format[i]//代表参数里对应的字符
    int i = 0;
    double sum = 0.0;
    va_list argptr;
    va_start(argptr, n); // 初始化argptr
    for (i = 0; i < n; ++i) {
        // 对每个可选参数,读取类型为double的参数,
        sum += va_arg( argptr, double); // 然后累加到sum中
    }
    va_end(argptr);
    return sum;
}

课后测试:

  • 0.我们说函数就是一种封装的方法,函数的设计应该遵从“一个函数仅实现一个功能”的原则,这样子我们就可以实现化繁为简的目的

    #include <stdio.h>
    char* myitoa(int num, char* str);
    
    //把整型转为字符串
    //参数:整型
    //返回值:返回字符串数组
    char* myitoa(int num)
    {
    	char str[32];
    	int dec = 1;
        int i = 0;
        int temp;
        if (num < 0){
            str[i++] = '-';
            num = -num;
        }
        temp = num;
        while (temp > 9){
            dec *= 10;
            temp /= 10;
        }
        while (dec != 0){
            str[i++] = num / dec + '0';
            num = num % dec;
            dec /= 10;
        }
        str[i] = '\0';
      	return str;
    }
    int main(void)
    {
        printf(%s\n”,myitoa(520));
        return 0;
    }
    
  • 1…不对,修改形参的值正常不会影响到实参,

  • 2.可以,作用提前结束函数并返回

  • 3.确保传参过程中指针地址里的值不会变

  • 4.等

  • 5.40;4

  • 6.9


30.指针函数和函数指针

指针函数(常见)和函数指针(用得不多)(可做参数、返回值(函数类型))

指针函数 -> int *p();

函数指针 -> int (*p)(int, int);

做参数:int call(int (*fp)(int, int), int num1, int num2)

做返回值int (*select(char ch))(int, int)

//函数指针数组

double (*func_table[4])(double, double) = {add, sub, mul, divi};//都是函数

这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即(*p);其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int(*)(int,int)。

需要注意的是,指向函数的指针变量没有 ++ 和 – 运算。

课后测试:

  • 0.函数名可以在表达式中被解读成“指向该函数的指针”。
  • 1.一个是指向函数地址的指针;一个是返回类型是指针的函数
  • 2.能,定义一个 void * 类型的指针函数。
  • 3.调用局部变量的地址
  • 4.char* a(char* b, void (*c)(int))
  • 5.func 是一个返回值为函数指针(指向一个参数为 int 类型,返回值为 void 类型的函数)的函数

31.局部变量和全局变量

课后测试:

  • 0.在模块化程序设计的指导下,我们应该尽量设计内聚性强,耦合性弱的模块。也就是要求你函数的功能要尽量单一,与其他函数的相互影响尽可能地少,而大量使用全局变量恰好背道而驰。
  • 1.两个都是局部
  • 2.用extern变量声明全局变量
  • 3.520;1;2;880
  • 4.野指针了;
char names[3][40];//C语言输入多个字符串得用二维数组或类似的
for (i = 0; i < 3; i++){
  printf("\n请输入%d号玩家的名字:", i+1);
  scanf("%s", names[i]);
}

32.作用域和链接属性

作用域:

代码块作用域:
最常见的就是代码块作用域。所谓代码块,就是位于一对花括号之间的所有语句。

int main(void)
{
   int i=888;
   {
       printf("7i = %d\n",i);//888
       int i=444;
        {
            int i=222;
            printf("11i = %d\n",i);//222
        }
        printf("13i = %d\n",i);//444
        {
            printf("15i = %d\n",i);//444
            int i=111;
            printf("17i = %d\n",i);//111
        }
        printf("19i = %d\n",i);//444
    }
    return 0;
}

文件作用域:
任何在代码块之外声明的标识符都具有文件作用域,作用范围是从它们的声明位置开始,到文件的结尾处都是可以访问的。另外,函数名也具有文件作用域,因为函数名本身也是在代码块之外。

原型作用域:
原型作用域只适用于那些在函数原型中声明的参数名。

函数作用域:
函数作用域只适用于 goto 语句的标签,作用将 goto 语句的标签限制在同一个函数内部。

链接属性:

在 C 语言中,链接属性一共有三种:
· external(外部的)-- 多个文件中声明的同名标识符表示同一个实体
· internal(内部的)-- 单个文件中声明的同名标识符表示同一个实体
· none(无)-- 声明的同名标识符被当作独立不同的实体(比如函数的局部变量,因为它们被当作独立不同的实体,所以不同函数间同名的局部变量并不会发生冲突)

默认情况下,具备文件作用域的标识符拥有 external 属性。也就是说该标识符允许跨文件访问。对于 external 属性的标识符,无论在不同文件中声明多少次,表示的都是同一个实体。
使用 static 关键字可以使得原先拥有 external 属性的标识符变为 internal 属性。这里有两点需要注意:
· 使用 static 关键字修改链接属性,只对具有文件作用域的标识符生效(对于拥有其他作用域的标识符是另一种功能)
· 链接属性只能修改一次,也就是说一旦将标识符的链接属性变为 internal,就无法变回 external 了

课后测试:

  • 0.作用域
  • 1.函数作用域
  • 2.是的,都是文件作用域
  • 3.一言以蔽之:声明和定义的主要区别就是是否为其分配内存空间
  • 4.外部链接属性
  • 5.none
  • 6.会报错,因为 goto 标签的作用域仅限于同一个函数体内部。
  • 7.那就是当你试图对一个参数为 void 的函数传入参数时,编译器会毫不犹豫地给予你错误提示!

33.生存期和存储类型

生存期:

C语言的变量拥有两种生存期
静态存储期(static storage duration)
自动存储期(automatic storage duration)
具有文件作用域的变量属于静态存储期,函数也属于静态存储期。属于静态存储期的变量在程序执行期间将一直占据存储空间,直到程序关闭才释放。

具有代码块作用域的变量一般情况下属于自动存储期。属于自动存储期的变量在代码块结束时将自动释放存储空间。

存储类型:
存储类型其实是指存储变量值的内存类型,
C语言提供了5种不同的存储类型:
auto register static extern typedef

自动变量(auto)
在代码块中声明的变量默认的存储类型就是自动变量,使用关键字auto来描述。 由于这是默认的存储类型,所以不写auto是完全没问题的。

寄存器变量(register)
将一个变量声明为寄存器变量,那么该变量就有可能被存放于CPU的寄存器中(CPU对寄存器读取存储几乎无延迟)。寄存器变量和自动变量(auto)在很多方面的是一样的,它们都拥有代码块作用域,自动存储期和空连接属性。不过这里有一点需要注意的是:当你将变量声明为寄存器变量,那么你就没办法通过取址运算符获得该变量的地址。

静态局部变量(static)
使用static来声明局部变量,那么就可以将局部变量指定为静态局部变量。static使得局部变量具有静态存储期,所以它的生存期与全局变量一样,直到程序结束才释放

static和extern
作用于文件作用域的static和extern,static关键字使得默认具有external链接属性的标识符变成internal链接属性,而extern关键字是用于告诉编译器这个变量或函数在别的地方已经定义过了,先去别的地方找找,不要急着报错。

课后测试:

  • 0.从内存地址释放的对应时间段
  • 1.存活到程序结束
  • 2.自动存储期
  • 3.寄存器存储类型
  • 4.代码块作用域;自动存储期;none
  • 5.链接属性:外部

地址存放顺序:

在这里插入图片描述


34.递归
  • 0.撒旦
  • 1.函数调用自身
  • 2.设置正确的结束条件
  • 3.效率比迭代慢、更吃内存空间、程序容易崩溃
  • 4.543210001234

递归必须设置正确的结束条件
结束条件大致分为://递归函数无参数用外部;有参数用内部
外部结束:递归过程中设置静态局部变量进行条件判断

void test(void)
{
  static int count = 10;
  if(--count)
	 test();
}

// 内部结束:在递归函数的参数上(做文章)进行条件判断
long fact(int num)//求阶乘!,5!=1*2*3*4*5
{
    long result;
    if(num > 0)
    	result = num * fact(num-1);
    else//num = 0
    	result = 1;
    return result;
}

35.汉诺塔;折半查找法
36.快速排序(Linux扩展-算法里面
37.动态内存管理(上)

都要free;// #include <stdlib.h>
malloc:void *malloc(size_t size);
malloc 函数向系统申请分配 size 个字节的内存空间,并返回一个指向这块空间的指针。
memset函数可初始化malloc的申请的内存空间。

calloc:void *calloc(size_t nmemb, size_t size);
calloc 函数在内存中动态地申请 nmemb 个长度为 size 的连续内存空间(即申请的总空间尺寸为 nmemb * size)
备注:calloc 函数与 malloc 函数的一个重要区别是:calloc 函数在申请完内存后,自动初始化该内存空间为零,而 malloc 函数不进行初始化操作,里边数据是随机的。

realloc:void *realloc(void *ptr, size_t size);

  • realloc 函数修改 ptr 指向的内存空间大小为 size 字节。
  • 如果新分配的内存空间比原来的大,则旧内存块的数据不会发生改变;如果新的内存空间大小小于旧的内存空间,可能会导致数据丢失。
  • 该函数将移动内存空间的数据并返回新的指针。
  • 如果 ptr 参数为 NULL,那么调用该函数就相当于调用 malloc(size)。
  • 如果 size 参数为 0,并且 ptr 参数不为 NULL,那么调用该函数就相当于调用 free(ptr)。
  • 除非 ptr 参数为 NULL,否则 ptr 的值必须由先前调用 malloc、calloc 或 realloc 函数返回。

导致内存泄漏主要有两种情况:
· 隐式内存泄漏(即用完内存块没有及时使用free函数释放)//忘记free
· 丢失内存块地址//不能free

int main(void)
{
    int *a;
    int c=1;
    a=(int*)malloc(sizeof(int));
    a=&c; //分配给a的4个字节的空间没有办法free了
    return 0;
}

课后测试

  • 0.size_t 实际上就是 unsigned int(无符号整型),在 64 位系统中是被定义为 long unsigned int。
  • 1.内存泄漏
  • 2.”难道调用 free() 函数释放内存后,ptr 不是应该指向 NULL 的吗?“。这里务必要注意一点,free() 函数释放的是 ptr 指向的内存空间,但它并不会修改 ptr 指针的值。也就是说,ptr 现在虽然指向 0x8b23008,但该空间已经被 free() 函数释放了,所以对于该空间的引用已经失去了意义(会报错)。因此,为了防止后面再次对 ptr 指向的空间进行访问,建议在调用 free() 函数后随即将 ptr 赋值为 NULL。`
  • 3.char* //二维、指针数组、数组指针实际上都是线性的
  • 4.因为将每次申请堆内存空间的粒度调大,malloc 函数会直接返回 NULL 表示失败,这样程序既不会因为内存耗尽而崩溃,也不会退出死循环

38.动态内存管理(下)

课后测试:

  • 0.不行吧

  • 1.str主要针对字符串;mem主要针对申请的动态内存

  • 2.int* p=(int *)malloc(1024*sizeof(int));

    memset(p, 0 ,1024*sizeof(int));

  • 3.malloc(1024);

  • 4.初始化静态变量时不能调用函数。static 声明的变量在程序运行过程中是始终存在的,通常在 main 函数运行之前就完成了初始化过程。但 malloc 函数的调用是在 main 函数之后进行的,所以从概念上来说,static 声明的变量不可能通过调用库函数来进行初始化。同样的道理,这个规则对于全局变量来讲也是一样的

    ……

    static int *pi;

    pi = (int *)malloc(sizeof(int));

    ……

此动动手对程序的抒写规范(高内聚,低耦合)以及一些小技巧

(1)剪刀1石头2布3与(2)(*3)剪刀3石头6布9//可数值相加做判断条件
7\11\6:(2)win…………
rand()%3+1;//不会包括0

矩阵用一维指针表示,申请内存时可多申请两个单位。用p[0]/p[1]装MN。
ptr = (int )realloc(ptr, (m * n + 2) sizeof(int));//方便其它函数获取M
N
其它函数:

int m = ptr[0];
int n = ptr[1];
int *matrix = ptr + 2;
for (i = 0; i < m; i++){
    for (j = 0; j < n; j++){
        matrix[i * n + j] = num;
    }
}

39.C语言的内存布局

1、代码段:
通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行之前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。程序段为程序代码在内存中的映射,一个程序可以在内存中有多个副本。

2、从静态存储区域分配(BSS段、数据段)
由编译器自动分配和释放,在程序编译的时候就已经分配好内存,这块内存在程序的整个运行期间都存在,直到整个程序运行结束时才被释放,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域。另外文字常量区,常量字符串就是放在这里。

2(1)、BSS段
通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS段属于静态内存分配。

2(2)、数据段
通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

3、从动态存储区域分配(栈和堆):

3(1)、在栈上分配(动态存储区域)(只有栈是从高地址到低地址发展)
同样由编译器自动分配和释放,在函数执行时,函数内部的局部变量都可以在栈上创建,函数执行结束时,这些存储单元将则被自动释放。
需要注意的是,栈内存分配运算内置于处理器的指令集中,它的运行效率一般很高,但是分配的内存容量有限。

3(2)、从堆上分配(动态存储区域)
也称为动态内存分配,由程序员手动完成申请和释放。程序在运行的时,由程序员使用内存分配函数(如 malloc 函数)来申请内存,使用完之后再由程序员自己负责使用内存释放函数(如 free 函数)来释放内存。
需要注意的是,如果在堆上分配了内存空间,就必须及时释放它,否则将会导致运行的程序出现内存泄漏(堆内存泄漏、系统资源泄漏)等错误。
在 C 语言中,不同类型变量的存储位置和作用域也有所不同。

在这里插入图片描述

课后测试:

  • 0.全局更小。
  • 1.因为这是存放在静态存储区的栈里,函数使用完自动释放地址。
  • 2.动态存储区栈上。
  • 3.这样做是为了防止堆或栈溢出导致代码被覆盖。
  • 4.内存碎片:从malloc基础实现原理来看:https://fishc.com.cn/thread-80955-1-5.html
  • 5.void (*array[3])(void);
  • 6.执行完 p = (int *)&b 之后,p 指针的 “眼界” 被强制调整为一个整形变量的宽度;如果这时候给它赋值,那么将 “污染” a 变量的地址空间(238-》100000010)。

0.malloc和 realloc 函数创建一个可以存放任意长度整数的容器(数字以字符的形式存储)。
在这里插入图片描述


40.高级宏定义(带参数)

宏定义的实质:机械替换。
C语言的三大预处理功能:宏定义(#define)、文件包含(#include)、条件编译(#ifdef;#else;#endif)。
不带参数的宏定义:替换操作。
#define PI 3.14

带参数的宏定义:
C 语言允许宏定义带有参数,在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数,这点和函数有些类似。
#define MAX(x,y) (((x)>(y))?(x):(y))

课后测试:

  • 0.宏定义、文件包含、条件编译
  • 1.机械替换
  • 2.空格
  • 3.虽然宏定义也有所谓的”形参“和”实参“,但在宏定义的过程中,并不需要为形参指定类型。这是因为宏定义只是进行机械替换,并不需要为参数分配内存空间
    而函数不同,在函数中形参和实参是两个不同的变量,都有自己的内存空间和作用域,调用时是要把实参的值传递给形参。
  • 4.没()
  • 5.++i、i++运算错误

0.用逆波兰表示法做一个四则计算器
· a - b * c + d:中缀表示法(Infix Notation),运算符在两个运算对象的中间。
· + - a * b c d:前缀表示法(Prefix Notation),运算符在运算对象的前面,又称波兰表示法。
· a b c * - d +:后缀表示法(Suffix Notation),运算符在运算对象的后面,又称为逆波兰表示法。


41.内联函数和一些鲜为人知的技巧(就是宏)

**C语言内联函数关键字:**现在的编译器也很聪明,就算你不写 inline,它也会自动将一些函数优化成内联函数。
inline (https://blog.csdn.net/21aspnet/article/details/6723896)

内联函数在编译层面类似于宏替换。也就是说,程序执行过程中调用内联函数不需要入栈出栈,所以效率会提高。

内联函数和宏(#definde)的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。(但节约时间的同时增加了空间的消耗)

#include <stdio.h>
inline int add(int x, int y);
int add(int x, int y) //or: inline int add(int x, int y){}
{
  return x+y;
}
int main()
{
  int i = 50;
  while(i--){
   	printf("%d\n",add(20,10));
  }
  return 0;
}
// 必须先声明再使用

高级宏的小技巧:
宏的预处理运算符:# 和 ##
和 ## 是两个预处理运算符。 在带参数的宏定义中,# 运算符后面应该跟一个参数,预处理器会把这个参数转换为一个字符串。

#define STR(s) # sprintf(STR(Hello %s,%n\n),STR(world),520);//字符串自带””
运算符被称为记号连接运算符,可以使用它来连接多个参数。
#define TOGETHER(x,y) x ## yprintf(%d\n”, TOGETHER(5,20));//520

可变参数的宏:
之前我们学习了如何让函数支持可变参数,带参数的宏定义也是可以使用可变参数的:

#define SHOWLIST() printf(#__VA_ARGS__)
//其中 ... 表示使用可变参数,__VA_ARGS__ 在预处理中被实际的参数集所替换。
#include <stdio.h>
#define SHOWLIST(...) printf(# __VA_ARGS__)
//#运算符后面的参数转化为字符串
int main(void)
{
    SHOWLIST(FishC, 520, 3.14\n);
    return 0;
}

可变参数可为NULL

#include <stdio.h>
#define PRINT(format, ...) printf(# format, ## __VA_ARGS__)
//#运算符后面的参数转化为字符串
//##拼接参数如果可变参数是空参数,## 会将 format 参数后面的逗号“吃掉”
int main(void)
{
    PRINT(num = %d\n, 520);
    PRINT(Hello FishC!\n);//可变参数为空
    return 0;
}

课后测试:

  • 0.main调用普通函数是去找它的定义,调用内联函数是把内容复制过来。
  • 1.死循环或多次调用内联函数
  • 2.记住,宏替换是机械愚蠢的,它是预处理命令,在代码编译前进行机械的替换。所以早在 num 被赋值为 520 之前,printf(“%s\n”, STR(num)) 就被替换为 printf(“%s\n”, “num”) 了。
  • 3.不加拼接##,不能为NULL
    这其实是符号连接运算符(##)的隐藏技能。如果可变参数是空参数,## 会将 format 参数后面的逗号“吃掉”,从而避免参数数量不一致的错误)^
  • 4.INT NL。
    因为 STR 宏先将 INT NL 变成了字符串 “INT NL”,那么字符串里面的内容自然是文本,是不会进行宏替换的。

#define FIFTH_ARG(A1, A2, A3, A4, A5, …) A5

#define ARGUMENTS(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, …) A11

番外:C结构体节省内存和增加访问速度的方法:

struct foo7 {
    char c;
    struct foo7 *p;
    short x;
};
// 将隐含的废液写明,形式如下:
struct foo7 {
    char c;            /* 1 byte */
    char pad1[7];          /* 7 bytes */
    struct foo7 *p;          /* 8 bytes */
    short x;           /* 2 bytes */
    char pad2[6];          /* 6 bytes */
};//24
// 重排:
struct foo8 {
    struct foo8 *p; /* 8 bytes */
    short x;        /* 2 bytes */
    char c;         /* 1 byte */
    char pad[5];    /* 5 bytes */

};//16

42.结构体

C99 增加了一种新特性:支持初始结构体的指定成员值。利用该特性,还可以不按结构体声明的成员顺序进行初始化:

struct Book book =             // 要先定义Book结构体
{
   .publisher = "清华大学出版社",
   .price = 48.8,
   .date = 20171111
};
// 注意:其它未初始化的数值型成员也将被自动初始化,其中数值型成员初始化为 0,字符型成员初始化为 '\0'。

课后测试:

  • 0.没有问题
  • 1.24(64位系统)
  • 2.64
  • 3.报错
  • 4.16;24
  • 5.改成struct A a={3,4};

接收字符串注意使用数组,别一天天的想用指针


43.结构体数组和结构体指针

结构体嵌套:
struct A
{
struct B b;
};

结构体数组:
struct 结构体名称
{
结构体成员;
};

struct 结构体名称 数组名[长度];
//struct Add add[3];

结构体指针://主要用来传递结构体参数
指向结构体变量的指针我们称之为结构体指针:
struct Book * pt;
这里声明的就是一个指向 Book 结构体类型的指针变量 pt。

我们知道数组名其实是指向这个数组第一个元素的地址,所以我们可以将数组名直接赋值给指针变量。
pt = add;
但注意,结构体变量不一样,结构体的变量名并不是指向该结构体的地址,所以要使用取地址运算符(&)才能获取其地址:
pt = &book;
通过结构体指针访问结构体成员有两种方法:
(结构体指针).成员名
结构体指针->成员名
第一种方法由于点号运算符(.)比指针的取值运算符(
)优先级要高,所以要使用小括号先对指针进行解引用,让它先变成该结构体变量,再用点运算符去访问其成员。
相比之下,第二种方法更加方便和直观。第二种方法使用的成员选择运算符(->)自身的形状就是一个箭头,箭头具有指向性,一下子就把它跟指针联系起来。
需要注意的是,两种方法在实现上是完全等价的.
注意:点号(.)只能用于结构体,而箭头(->)只能用于结构体指针,这两个就不能混淆。

课后测试:

  • 0.(32位)224

  • 1.不行,需要book.date.year;

  • 2.720

    struct Student stu=
    {
        .id=123,
        .name=”sr”,
        .date.year=2021,
        .date.month=07,
        .date.day=16,
        .date.time.hour=18,
        .date.time.minute=30,
        .date.time.second=59
    };
    struct Student stu = {123, "FishC", {2021, 07, 16, {16, 58, 00}}};
    
  • 4.一

    printf(%d:%d:%d”,pt->date.time.hour......);
    

用scanf(“%s”)获取字符串,别一天天想着用while(getchar())。


44.传递结构体变量和结构体指针

相同结构体类型的变量可直接赋值:

struct Test
{
    int x;
    int y;
}t1, t2;
t1.x = 3;
t1.y = 4;
t2 = t1;

结构体变量可做为参数传递,和变量一样使用;
不过为了程序效率,更多的是使用结构体指针做为参数传递。

动态申请结构体:

typedef struct Book
{
  char title[64];
  char author[32];
  float price;
  struct Date date;
  char publisher[64];
}Book,*pBook;

Book *library[MAX_SIZE];
library[0] = (Book *)malloc(sizeof(struct Book));

课后测试:

  • 0.报错

  • 1.t3错

  • 2.未知

  • 3.b1,b2不是指针不能作为内存申请的返回值

  • 4.test->x=3;test->y=4;

    struct Test* setTest(int x, int y)
    struct Test* pt;
    

45~47.单链表
  • 0.单链表对链表成员的各种有序改动更容易实现

  • 1.存储空间连续,访问更快
    对于单链表来说,随机访问中间的某一个元素,都是需要大量的跳转操作(从第一个元素开始,然后通过指针一个一个往后跳,最后抵达目标位置),而数组则完全没有这个问题(直接通过下标索引值一步到位地访问)。

  • 2.单链表:容易有序插入删除添加。

  • 3.报错;无限递归

  • 4.free(temp);

  • 5.因为main里面library没有申请内存,并且addBook函数更改了library结构体指针。
    传值和传地的区别(虽然都是传值):
    **的内存存储结构:
    在这里插入图片描述

    *的为:
    在这里插入图片描述

    addBook(library) 传递的是 library 指针的值,也就是把 NULL 传过去了;而 addBook(&library) 传递的是 library 指针的地址,自然,传过去的是指针的地址,那么要接住它就必须使用指向指针的指针。

课后测试:

  • 0.对,因为单链表除了有“头”有“尾”,每个数据节点还得存放一个指向下一个节点的指针。//同样的数据,存储在单链表中会比存储在数组中至少多占用一倍的空间
  • 1.按正常逻辑顺序存储
  • 2.定义一个静态结构体指针指向单链表尾部
  • 3.最好为NULL;最坏为在最后一个或不存在
  • 4.完全一样,释放空间跟打印数据(void printLibrary(struct Book *library))一样,只需要能够拿到每个成员的地址,就可以释放它。//因为library已经不为NULL了。

0.(都是编程)
视频里编程思路,指针妙用。赋予指针适合的意义和指向,可清晰编程逻辑。
指针巨巨巨好用,多加注意。

编码集合:太多了,看test1_45~47.c


48.内存池

内存池是针对内存碎片机制进行优化的一种方法并能减少时间上的消耗。
内存池其实就是让程序额外维护一个缓存区域。
实现手段可:创建一个内存池(单链表)(规定MAX(1024),count),当需要申请内存时,先判断内存池里是否有空闲空间,优先从内存池获取空间,如果没有再malloc。
free空间时,把需要释放的空间放置于内存池中,如果内存池满空闲时就free。

编码集合:看test1_47(3).c


49.基础typedef

相比起宏定义的直接替换,typedef是对类型的封装起别名,typedef可以一次性起多个别名。

// typedef与define的不同:
typedef int INTEGER;//起别名,但新别名只能继承作用,不能继承延伸
#define INT int 
int main(void)
{
  unsigned INT a = -1;//INT => INTEGER会报错
  printf("%d\n",a); 
  return 0;
}

50.进阶typedef
#include <stdio.h>
// int (*ptr)[3];
typedef int (*PTR_TO_ARRAY)[3];// 数组指针

// int (*fun)(void);      // int *(*array[3])(int);
typedef int (*PTR_TO_FUN)(void);// 函数指针

// void (*funA(int, void (*funB)(int)))(int)
typedef void (*PTR_TO_FUN2)(int);
PTR_TO_FUN2 fun(int, PTR_TO_FUN2);

int funA(void)
{
    return 520;
}
int funB(void)
{
    return 1314;
}
int funC(void)
{
    return 999;
}

int main(void)
{
    int str[3] = {1, 2, 3};
    PTR_TO_ARRAY ptr = &str;	// 指针指向数组[3]的地址
    for (int i = 0; i < 3; i++){
        printf("%d\t",(*ptr)[i]);
    }printf("\n");
    
    PTR_TO_FUN pfun = &funA;
    printf("%d\n",(*pfun)());

    PTR_TO_FUN array[3] = {&funA, &funB, &funC};
    for (int i = 0; i < 3; i++){
        printf("%d\t",(*array[i])());
    }printf("\n");
    
    return 0;
}


51.共用体
typedef union Lab
{
    int a;//4
    char chr;//2
    struct Student st;//104
}lab;
 
int main(void)
{
    struct Student stu;//sizeof == 104
    lab l;
    printf("%d\n",sizeof(l));//104
 
    l.a = 10;
    printf("%d\n",l.a);//10
    l.chr = 'a';
    printf("%d\n",l.a);//97
    printf("%c\n",l.chr);//a
    return 0;
}

共用体(联合体):
结构体(Struct)是一种构造类型或复杂类型,它可以包含多个类型不同的成员。在C语言中,还有另外一种和结构体非常类似的语法,叫做共用体(Union),它的定义格式为:

union 共用体名{
成员列表
};

结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。

union data
{
    int n;
    char ch;
    double f;
}a, b, c;
// 共用体 data 中,成员 f 占用的内存最多,为 8 个字节,所以 data 类型的变量(也就是 a、b、c)也占用 8 个字节的内存,并且需要考虑数据对齐问题。

52.枚举类型
enum Week {sun, mon, tues, wed, thur, fri, sat};//0~6
enum Week {sun = 1, mon, tues, wed = 7, thur, fri, sat};//12378910

枚举值定义后变为常量,不能在程序中用赋值语句再对它赋值。
枚举变量:
enum Week today ?= int today;//实验多次感觉没区别


53.位域

位域、位段、位字段都是一种东西。
使用位域的做法是在结构体定义时,在结构体成员后面使用冒号(:)和数字(不能超过所依附的数据类型位数的长度)来表示该成员所占的位数。

typedef struct Student//位域结构
{
    unsigned int a:1;
    unsigned int b:1;
    unsigned int c:2;//33报错,because:sizeof(unsigned int)==4;4*8==32
}stu;

stu st;
printf("%d\n",sizeof(st));//实为1,数据对齐变4
st.a = 1;
st.b = 0;
st.c = 3;
printf("%d,%d,%d\n",st.a,st.b,st.c);//1,0,3

无名位域:
无名位域的目的只有一个,就是为了进行填充和对齐。因为某些协议中规定了某些位是保留位,暂时不使用,两个需要使用的位之间如果有几个bit是不使用的,就需要通过无名域进行调整。
也可将其前后的两个位域或成员分开放在两个字中。

typedef struct Student
{
   	unsigned int a:1;
    unsigned int  :7;//1字节
    unsigned int b:5;
    unsigned int  :3;//1字节
    unsigned int d:16;//2字节
}stu;

不能用来指定位数的类型:
如果struct成员是指针变量不能用来指定所占的位数;struct成员是double或float类型,也不能指定位数,否则编译出错,位域类型无效。


54.位操作

C语言并没有规定一个字节的尺寸(1字节不一定等于8位,需要看编译器。不过基本都是8位,少于8位的需要到博物馆寻找)
(32/64位系统是看size_t是多少个字节)(拓展:在同一时间中处理二进制的位数叫字长。32位系统的字长为32;64为64)

逻辑位运算符(除~其它运算符可结合=:&=;^= ;|=)
~:按位取反 优先级高
&:按位与 中
^:按位异或 低
|:按位或 最低


55.移位和位操作的应用

移位运算符(可结合=:<<=;>>=)186
(左移)<<:位移上,10111010 << 2 => 0010 1110 1000 数值上,*2^2
(右移)>>:位移上,10111010 >> 2 => 00101110 数值上,/2^2(取值与int类似不计余数)

位操作应用(突出掩码)
掩码:掩码是一串二进制代码对目标字段进行位与运算,屏蔽当前的输入位。
将源码与掩码经过按位运算或逻辑运算得出新的操作数。其中要用到按位运算如|运算和&运算。用于如将ASCII码中大写字母改作小写字母。 如A的ASCII码值为65= (01000001)2,a的ASCII码值为97=(01100001)2,要想把大写字母A转化为小写字母只需要将A的ASCII码与(00100000)2进行或运算就可以得到小写字母a。(小变大异或32)

1表示通电,0表示断开

在这里插入图片描述

在这里插入图片描述

拓展:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  int a = 12;
  printf("八进制 --> %o\n", a);
  printf("十六进制 --> %X\n", a);
  printf("十进制 --> %d\n", a);
  char s[10];
  itoa(a, s, 2);
  printf("二进制 --> %s\n", s);
  itoa(a, s, 3);
  printf("三进制 --> %s\n", s);
  return 0;
}

56.打开和关闭文件
57.读写文件1
58.读写文件2

主要为各种文件操作函数:

  • fopen/fclose:函数fopen有两种打开文件的模式。一种文本模式;一种二进制模式(+b)。
  • fgetc/fputc:getc/putc,功能和描述上一样,只是实现方式上有区别:函数;宏
  • fgets/fputs:专读写字符串
  • fread/fwrite:读写大量其它数据,比如结构体
  • fseek/feof:移动文件光标(指示器)/检测文件末尾光标
  • fscanf/fprintf:从文件里读取格式化字符串/写入格式化字符串到文件里
#define MAX 1024

int main(int argc, char **argv)
{
 // 成功返回一个指向文件结构的指针,失败null,并置位errno
 FILE *p1;
 if((p1 = fopen("./lab1.txt", "r+")) == NULL){
   perror("打开文件lab1.txt失败!");
   exit(EXIT_FAILURE);
 }

 FILE* p2 = fopen("./lab2.txt", "r+");
 // 成功返回读到/写入的字节数。失败返回0
 char buf[MAX] = {0};// 缓冲区
 int fread_ret = fread(buf, sizeof(char), MAX, p1);
 int fwrite_ret =fwrite(buf, sizeof(char), fread_ret, p2);
 printf("%d\n",fwrite_ret);
   
 // 改变文件流,成功0;失败-1
 fseek(p1, 0, SEEK_SET);

 int i = 40;
 fprintf(p1, "succes,%d\n",i);
 fclose(p1);
 fclose(p2);
 return 0;
}

fprintf( ) 函数中格式化的规定与printf( ) 函数相同,所不同的只是 fprintf()函数是向文件中写入。而printf()是向屏幕输出。

fopen f-磁盘操纵
一般用fopen打开普通文件,用open打开设备文件
fopen是标准c里的,而open是linux的系统调用.他们的层次不同.fopen可移植,open不能.

我认为fopen和open最主要的区别是fopen在用户态下就有了缓存,在进行read和write的时候减少了用户态和内核态的切换,而open则每次都需要进行内核态和用户态的切换;表现为,如果顺序访问文件,fopen系列的函数要比直接调用open系列快;如果随机访问文件open要比fopen快。

来自论坛的经典回答:open、fopen:
前者属于低级IO,后者是高级IO。
前者返回一个文件描述符(用户程序区的),后者返回一个文件指针。
前者无缓冲,后者有缓冲。
前者与 read, write 等配合使用, 后者与 fread, fwrite等配合使用。
后者是在前者的基础上扩充而来的,在大多数情况下,用后者。
https://blog.csdn.net/lyj2014211626/article/details/71844122

各个操作系统对一些特殊字符的表示不尽相同,比如 C 语言是使用 \n 表示换行符,在 Windows 上换行符却是用 \r\n 来表示,而 Mac 上则是 \r,只有 UNIX 系统的换行符跟 C 语言一致。所以如果在 Windows 中打开一个文本文件(文本流),系统将 \r\n 自动转换为 \n(以满足 C 标准),而如果是写入文本文件,则将 \n 有转换成 \r\n 来存放。

还有一个是二进制流,相比起文本流来说,二进制流是“透明的”记录内部数据,从二进制流读取的数据始终等于之前写入到该流的数据,不会做任何自动的转换。

想要把数值写入文件中并正常查看:
需要把数值(二进制)转字符串(文本),并写入到文件中,才能在文件中正常显示,否则直接写入显示的为二进制模式(需要用xxd命令看(1、查看数据的二进制;2、查看字符串的ASCLL码)(并注意大小端显示问题)(xxd file.txt))。


59.随机读写文件

ftell:返回文件光标的位置。

可移植性问题:
对于以二进制模式打开的文件,fseek函数在某些操作系统中可能不支持SEEK_END位置。

对于以文本模式打开的文件,fseek函数的whence参数只能取SEEK_SET才是有意义的,并且传递给offset参数的值要么是0, 要么是上一次对同个文件调用ftell函数获得的返回值。


60.标准流和错误处理

标准流:
文件三个标准流(终端):标准输入(stdin)、标准输出(stdout)、标准错误输出(stderr);(这些都可以当作FILE*用)

ferror:检测文件的错误指示器(例如对一个以只读的方式打开的文件写入数据时,就会设置错误指示器)是否被设置。(只能检测是否出错,不能获取原因)(大多数系统函数会把错误原因记录在errno中)(#include <errno.h>)(更多的时候用perror函数或用strerror函数返回错误码信息)

clearerr:清除指定文件的末尾指示器(feof)和错误指示器(ferror)的状态。

拓展:
重定向:(可组合使用)

  • -重定向标准输入使用<
  • -重定向标准输出使用>:./a.out > output.txt
  • -重定向标准错误输出使用2>./a.out 2> error.txt

61.IO缓冲区

IO缓冲区:
Linux 的标准函数库中,有一种被称作“缓冲 I/O”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。

在这里插入图片描述

标准IO提供的三种类型的缓冲模式:

  • (1)按块缓存:也称全缓存,在填满缓冲区后才进行实际的设备读写操作。
  • (2)按行缓存:指在接收到换行符('\n’)之前,数据都是先缓存在缓冲区的
  • (3)不缓存:允许你直接读写设备上的数据

// setvbuf:用于指定一个数据流的缓存模式。
// fflush:将缓冲区内的数据强制写入指定的文件中。

#include <stdio.h>
#include <string.h>
#define MAX 1024

int main(void)
{
  char buff[MAX];
  memset(buff, '\0', MAX);
  
  setvbuf(stderr, buff, _IOFBF, MAX);//_IONBF
  fprintf(stderr, "welcome to here\n");
 
  fflush(stderr);
  fprintf(stderr, "hello world\n");
  getchar();
  return 0;
}

推荐阅读

https://blog.csdn.net/weixin_45068267

;