当前位置: 首页 > article >正文

C语言学习笔记 (初阶)

仅是自己的学习记录,内容难免有错,欢迎大家批评指正。

其他

  • 转义字符: \ddd是八进制,\xdd是十六进制

  • 注释:

    • C语言版本: /* */ 这种是不支持嵌套的
    • C++版本:// 推荐使用这种
  • 数组

    int a[10];

    int a[3] = {1,2};// 不完全初始化,关于char字符串的长度(strlen()函数)

    C99之前,数组的长度只能是常量

    C99之后,支持变长数组,允许数组的长度是变量,但是这种指定的方式是不能初始化的

    VS不支持C99的标准

  • break语句把程序控制从包含该语句的最内层的while、do、for、switch语句中转移出来

  • break语句只能跳出一层嵌套

  • goto语句

    • goto语句只能转到本函数中指定标号的位置,且goto语句不可用于绕过边长数组的声明
    • goto语句对于嵌套循环的跳出很有用,因为break只能跳出当前的循环(也就是跳出多层循环,因为使用多层循环时,break要使用多次)
    • goto语句不能跨函数
  • 无限循环格式:while(1)for(;;)两种都可以,现代编译器上两者没有区别

  • system("cls"); // 库函数,执行系统命令, 这句话的意思就是吧“”里的内容传给cmd执行

  •   int isPrime(int a)
      {
      	// 素数只能被1和它本身整除
      	if (a % 2 == 0 && a>2) return 0;  // 偶数都不是素数
      	int i = sqrt(a);				// 这一句是值得优化的
      	while (i > 1) {
      		if (a % i == 0) return 0;
      		i--;
      	}
      	return 1;
      }
    

代码规范

if ( 2 == a );	// 推荐用这种写法(把变量放在右边,常量放在左边)
if ( a == 2 );	// 如果少写一个=编译器是检查不出来的

在VS中按下F1查看帮助

关机指令

C:\Users\JTL_1>shutdown -s -t 120

C:\Users\JTL_1>shutdown -a

堆区 & 栈区 & 静态区

  • 栈区(栈区的变量都是临时性的):局部变量、函数的形参、函数的调用、函数的返回值
  • 堆区:动态分配的内存(malloc,calloc,realloc)
  • 静态区:静态变量、全局变量

根据时间戳产生一个随机数(0~100)

  • 时间戳:

    time();		// 函数返回一个时间戳,它需要一个time_t*类型的参数,可以传入空
        
    #include<math.h>
    #include<Windows.h>
    #include<time.h>
    srand((unsigned int)time(NULL));  // 随机数种子只用初始化一次,放在程序执行的最前面即可,通常是main()函数的第一句
    int target = rand() % 101;
    

逗号表达式

// 调用函数
func((v1,v2),(v3,v4),v5,v6,v7);	 // 这里一共5个实参,其中(v1,v2)是一个逗号表达式,整体算一个实参
  • 逗号表达式的计算是从左到右依次计算,整个逗号表达式的结果是最后一个表达式的结果:

    return 3,4,5;    // 最后返回的结果是5
    

bool、true、false

  • C99中支持bool,true,false等关键字,需要包含头文件 <stdbool.h>

  • 在不支持C99的编译器中,可以通过以下方法实现:

    #define true 1
    #define false 0
    tpedef int bool;
    

exit()函数

  • 在main()函数中的exit()函数和return的作用相同

    exit(EXIT_FAILURE);  // 属于<stdlib.h>头文件,
    exit(EXIT_SUCCESS);  // 表示程序正常终止,也可以写成exit(0)
    

Switch语句

  • switch语句如果没有break,会一直穿透,default也穿透

  • switch语句可以没有default标号,defalult标号的位置随意

  • 在case子句中,即使有多个执行语句,也不必打花括号(这是C语言中唯一不用加括号的)

  • 在执行完一个case标号后的语句后,就从此标号执行下去,不再进行判断

  • 最后一个case语句或switch语句中,可以不用加break,因为流程已经达到了switch结构的结束处

操作符

算数操作符

  • % 取模操作符的两边必须是整数

  • sizeof是单目操作符,不是函数

  • 强制类型转换: (int)3.14

  • extern关键字

    test1.c
    
    int sum = 12;
    
    test2.c
    
    // 要使用test1.c中的sum变量
    extern int sum;
    
    printf("%d",sum); // 12
    

    通过extern关键字可以访问其他源文件中的全局变量(函数也是类似)

移位操作符

  • C语言的移位操作符只能对整数起作用,其他类型不能用移位操作符

  • 正数 原码、反码、补码相同

  • 负数 原码最高位符号位为1 || 原码的符号位不变,其他位置按位取反就得到反码 || 反码末位加1就得到补码,反码的符号位也不变

  • 计算机在内存中都是按补码存放的

  • 移位操作符,移动的是二进制位

    正整数的左移位操作
    int a = 7;
    int c = a << 1;	 // a算数左移1位,高位舍弃,低位补0
    printf("a = %d\n", a);	// a = 7 = 0111
    printf("c = %d\n", c);  // c = 14 = 1110
    
    负整数的左移位操作
    int a = -7;     // -7的原码:1 000 0111,反码:1 111 1000,补码:1 111 1001
    int c = a << 1; // a的补码左移一位都得到的补码:1 111 0010,对应的原码为再次对补码按位取反再加 1,高位舍弃,低位补0
    printf("a = %d\n", a);	// a = -7 = 1 000 0111
    printf("c = %d\n", c);  // c = -14 = 1 000 1110
    
  • 无论正数还是负数,左移都有×2的效果

  • 右移:分为算数右移和逻辑右移

    • 算数移位:右边的丢弃,左边补原来的符号位,也就是正数补0,负数补1 (VS编译器采用的是算术右移)
    • 逻辑移位:右边的丢弃,左边的补0
    正整数的右移位操作
    int a = 7;
    int b = a >> 1;	 // a算数右移1位,低位舍弃,高位补0
    printf("a = %d\n", a);	// a = 7 = 0111
    printf("b = %d\n", b);  // b = 3 = 0011,对于正整数,算数右移和逻辑右移的效果相同
    
    负整数的右移位操作
    int a = -7;		// -7的原码:1 000 0111,反码:1 111 1000,补码:1 111 1001
    int b = a >> 1;	// a的补码算术右移一位后得到的补码:1 111 1100,对应的原码为再次对补码按位取反再加 1,高位补1,低位舍弃
    printf("b = %d\n", b);  // b = -4 = 1 000 0100
    

位操作符

  • 位操作符只适用于整数
  1. & 按位与,位是指2进制位,符号位参与运算(同1异0)

    int a = 3;		// 补码:  3 = 0 000 0000 0000 0000 0000 0000 0000 0011    (4B = 32bit)
    int b = -5; 	// 补码: -5 = 1 111 1111 1111 1111 1111 1111 1111 1011
    int c = a & b;	// 补码按位与: 0 000 0000 0000 0000 0000 0000 0000 0011 = 3  正数补码=自己的原码
    printf("%d\n", c);  // c = 3
    
  2. | 按位或,符号位参与运算

    int a = 3;		// 补码:  3 = 0 000 0000 0000 0000 0000 0000 0000 0011    (4B = 32bit)
    int b = -5; 	// 补码: -5 = 1 111 1111 1111 1111 1111 1111 1111 1011
    int c = a | b;	// 补码按位或: 1 111 1111 1111 1111 1111 1111 1111 1011 = -5的补码
    printf("%d\n", c);  // c = -5
    
  3. ^ 按位异或,符号位参与运算(同0异1)

    int a = 3;		// 补码:  3 = 0 000 0000 0000 0000 0000 0000 0000 0011    (4B = 32bit)
    int b = -5; 	// 补码: -5 = 1 111 1111 1111 1111 1111 1111 1111 1011
    int c = a ^ b;	//补码按位异或:1 111 1111 1111 1111 1111 1111 1111 1000 = -8的补码
    printf("%d\n", c);  // c = -8
    
  4. 例题,不创建临时变量,实现两个数的交换

    int a = 3;
    int b = 5;
    
    法一:(这种方法有局限,因为a+b可能溢出)
    a = a + b;
    b = a - b;
    a = a - b;
    
    法二:(三次异或实现交换)
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    
    • 两个相同的数异或得0,0异或一个数仍得此数
    • 异或操作符支持交换律:a^b^c = a^c^b
  5. ~ 按位取反,位是二进制位,符号位参与运算

    int a = 3; 	// 补码: 3 = 0 000 0000 0000 0000 0000 0000 0000 0011
    int b = ~a; // 补码:	   1 111 1111 1111 1111 1111 1111 1111 1100 = -4的补码
    					    1 000 0000 0000 0000 0000 0000 0000 0011 = -4的反码
                              1 000 0000 0000 0000 0000 0000 0000 0100 = -4的原码
    
  6. 例题:修改整数的二进制的某一个位

    int a = 13;
    // 13 = 0 000 0000 0000 0000 0000 0000 0000 1101		// 如果要修改倒数第二位为1
    //      0 000 0000 0000 0000 0000 0000 0000 0010        // 或上这么一个数就可以了
    //      0 000 0000 0000 0000 0000 0000 0000 0001		// 1左移一位就是上边这个数 即 1<<1
    int b = a | (1 << 1);
    printf("%d\n", b);  // b = 15
    
    所有以后,修改a的倒数第n位为1,就让a按位或上一个 (1 << n-1)
    

下标引用操作符[]

对于 [ ] 操作符,它的两个操作数是可以交换的

int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
arr[7] = 99; // 此时 对于操作符[] 它有两个操作数,一个是arr, 一个是7
7[arr] = 99; // 这是可以的,对于 [] 操作符,它的两个操作数是可以交换的
  • arr[7] 等价于 7[arr] 等价于 *(arr + 7) 等价于 *(7 + arr)

函数调用操作符()

  • ()至少有一个操作数,那就是函数名,可以没有参数
int add(int x, int y){
	reutrn x + y;
}

int main(void){
    int a = 10, b = 20;
    int res = add(a,b);  // ()就是函数操作符,他有三个操作数,add,a,b
    return 0;
}

格式化输入输出

  • %2d 两位右对齐 (默认右对齐)

  • %-2d 两位左对齐

  • %p 表示输出以内存中实际存储一个变量格式(十六进制、32位(视机器而定))的值,也即是地址

  • %u 表示无符号整数输出

    #include <stdio.h>
    
    int main() {
        int num = 20;
        float pi = 3.14159;
        char ch = 'A';
        char str[] = "Hello, C!";
        
        // 使用不同的格式字符
        printf("整数输出 (decimal): %d\n", num);  // 输出: 20
        printf("无符号整数输出 (unsigned): %u\n", num);  // 输出: 20
        printf("浮点数输出: %f\n", pi);  // 输出: 3.141590
        printf("字符输出: %c\n", ch);  // 输出: A
        printf("字符串输出: %s\n", str);  // 输出: Hello, C!
        printf("十六进制输出 (小写): %x\n", num);  // 输出: 14
        printf("十六进制输出 (大写): %X\n", num);  // 输出: 14
        printf("指针地址输出: %p\n", (void*)&num);  // 输出: 地址值
        printf("输出百分号: %%\n");  // 输出: %
        printf("科学记数法 (小写 e): %e\n", pi);  // 输出: 3.141590e+00
        printf("科学记数法 (大写 E): %E\n", pi);  // 输出: 3.141590E+00
        printf("自动选择浮点数格式: %g\n", pi);  // 输出: 3.14159
        
        
        int num = 5;
        int width = 5;
        float pi = 3.14159;
        float largeNum = 12345.6789;
        char str[] = "Hello, World!";
        
        // 1. 数字对齐与宽度控制
        printf("1. 数字对齐与宽度控制:\n");
        printf("|%2d|\n", num);  // 输出: | 5|,右对齐,宽度 2
        printf("|%-2d|\n", num); // 输出: |5 |,左对齐,宽度 2
        printf("|%3d|\n", num);  // 输出: |  5|,右对齐,宽度 3
        printf("|%05d|\n", num); // 输出: |00005|,宽度 5,填充零
        printf("|%*d|\n", width, num);  // 输出: |   5|,宽度 由变量指定
        printf("\n");
    
        // 2. 浮点数精度控制
        printf("2. 浮点数精度控制:\n");
        printf("|%8.3f|\n", pi);  // 输出: |   3.142|,宽度 8,小数点后 3 位
        printf("|%8.2f|\n", pi);  // 输出: |   3.14 |,宽度 8,小数点后 2 位
        printf("|%8.4f|\n", pi);  // 输出: |  3.1416|,宽度 8,小数点后 4 位
        printf("\n");
    
        // 3. 自动选择浮点数格式
        float pi = 3.14159;
        float largeNum = 12345.6789;
        printf("3. 自动选择浮点数格式:\n");
        printf("|%g|\n", pi);  // 输出: 3.14159,自动选择合适的格式
        printf("|%g|\n", largeNum);  // 输出: 12345.7,自动选择合适的格式
        printf("|%g|\n", 0.000012345);  // 输出: 1.2345e-05,自动选择科学记数法
        printf("\n");
    
        // 4. 字符串的最大输出长度控制
        printf("4. 字符串的最大输出长度控制:\n");
        printf("|%10.5s|\n", str);  // 输出: |Hello     |,最多输出 5 个字符,宽度为 10
        printf("|%5.5s|\n", str);   // 输出: |Hello|,最多输出 5 个字符,宽度为 5
        printf("\n");
    
        // 5. 输出指针地址
        printf("5. 输出指针地址:\n");
        printf("Address: %p\n", (void*)&num);  // 输出指针的地址
        printf("\n");
    
        // 6. 输出百分号
        printf("6. 输出百分号:\n");
        printf("%%\n");  // 输出: %
        printf("\n");
    
        // 7. 科学记数法格式
        printf("7. 科学记数法格式:\n");
        printf("%e\n", pi);  // 输出: 3.141590e+00,科学记数法(小写 e)
        printf("%E\n", pi);  // 输出: 3.141590E+00,科学记数法(大写 E)
        printf("\n");
    
        return 0;
    }
    

关键字

static关键字

  1. 修饰局部变量(静态局部变量)
    1. 当局部变量出了块作用域,不销毁(本质上,static修饰存储变量时,改变了变量的存储位置)。局部变量本来是存放在栈区的,当static修饰时,该变量就放在了内存的静态区,当程序的生命周期结束时,该变量才销毁(在程序执行期间,它占据的内存单元是不会变的)
    2. 静态局部变量始终具有块作用域(指:从变量声明的点开始一直到所在函数体的末尾),所以它对其他函数是不可见的。也就是说,静态局部变量是对其他函数隐藏数据的地方,但是他会为将来同一个函数的再次调用保留这些数据
    3. 感受到的效果是作用域扩大了
  2. 修饰全局变量(静态全局变量)
    1. 全局变量是具有外部链接属性的
    2. 而static修饰的全局变量,全局变量的外部链接属性就变成了内部链接属性,其他源文件(.c文件)就不能再访问到该全局变量
    3. 感受到的效果是,变量的作用域变小了
  3. 修饰函数(静态函数)
  4. 函数也具有外部链接属性,可以通过extern关键字访问不同源文件中的函数
    2. 使用static修饰的函数,其外部链接属性就变成了内部链接属性,其他源文件一样的也不能再访问到该函数

其他

  • register关键字

    • register修饰的变量是建议编译器把他存在寄存器中,其访问速度更快,但是最终是不是存在寄存器中还是由编译器决定
    • 当一个变量需要被大量重复的使用时,可以使用register来提升速度
    • 但是现在的编译器,即使不指定,如果寄存器有空间,也会自己放进寄存器中来提高速度
  • define定义常量和宏

    •   // define定义并标识符常量
        #define MAX 123
        
        // define定义宏 (宏是有参数的)
        #define ADD(x,y) ((x)+(y))
        |define|  宏名   |宏的作用(宏体)|
            x,y是宏的参数,它们是没有类型的,
            在代码中最终会替换成宏体
      
int* p = &a;  // 指针变量
&a;	// 取地址操作符
*p;	// 解引用操作符,根据p中存放的地址,找到p指向的对象
  • 任何类型的指针变量的大小都是一样大的,因为指针变量都是用来存放地址的,而计算机中地址的长度是固定的,如32位机就是4B,64位机就是8B; X86就是32位平台,X64就是64位平台
  • 指针变量的大小取决于地址的大小

结构体

struct Stu{
	int id;
    char name[20];
};
//  定义一个结构体类型是不占用空间了

struct Stu s;	// 这样声明了后才是分配空间

define不是关键字,是预处理指令

IO函数

puts()函数和printf()函数的区别:

相同之处:都是用于打印数据的语句,都在<stdio.h>头文件中。puts()函数的作用与printf(“%s\n”,str);语句相同

不同之处:

特性printf()puts()
输出格式可以输出多种类型的数据,并可以自定义格式只输出字符串,不能进行格式化输出
格式控制支持格式化输出,可以控制宽度、精度等不支持格式化,只能输出完整的字符串
结尾符不自动添加换行符自动在输出的字符串末尾添加换行符
返回值返回输出的字符数(成功时)返回一个整数值:0 表示成功,EOF 表示失败
使用场景适用于复杂的输出,能够根据需要自定义输出格式适用于简单的字符串输出,且自动换行

字符串

  • 字符串以 \0 结束,字符串长度不包括\0,以{}给字符串赋值时,必须手动添加\0结束符

  • 比较两个字符串是否相同

    strcmp(input, password) == 0;  // 如果返回值等于0说明字符串相同,否则两个字符串不同
    

strlen()和sizeof()的区别

  • sizeof是运算符,strlen是函数

  • **sizeof()在编译时运算,而strlen()**的结果在运行时才能计算出来

  • strlen()是专门用于求字符串长度的库函数,只能用于字符串,从参数给定的地址一直向后\0,统计\0之前出现的字符个数,不包含 \0

  • sizeof()操作符计算变量,或者类型所创建的变量占据内存空间的大小,单位是字节,只关注内存空间的大小,不在乎内存中的内容,因此求字符串的长度,包含 \0

    int a = 10;
    sizeof(a) 或者 sizeof(int) 或者 sizeof a 都是可以的
    
    char str[] = "abcde";
    printf("%d\n", sizeof(str));	// 6
    printf("%d\n", strlen(str));	// 5

数组

一维数组的创建和初始化

  • 数组在内存中是连续存放的

  • 数组的创建(C99之前,数组的长度只能是常量或常量表达式)

    int a[10];   
    char b[20];
    double c[30];
    
    // 下边的代码只能在支持C99标准的编译器上编译(如GCC)
    // C99之后,数组的大小可以是变量,是为了支持变长数组,变长数组是指数组的长度是通过变量来指定的,而不是数组的长度可变
    int n = 10;
    int arr[n];	// 这种数组是不能初始化的
    
  • 数组的初始化

    int arr[10] = {1, 2, 3};		// 不完全初始化,剩余的元素默认初始化为0
    int arr[10] = {0};			    // 把所有元素初始化为0
    
    char ch1[10] = {'a', 'b', 'c'};  // 剩余元素初始化为'\0'也就是0,其内容是 a b c 0 0 0 0 0 0 0
    char ch2[10] = "abc";			// 这种方式初始化字符数组也可以,其内容是 a b c \0 0 0 0 0 0 0 ,这种方式会多存一个 \0
    
    int arr[] = {1, 2, 3};			// 没有指定数组元素的个数时,编译器就会根据初始化的内容来推算元素的个数
    
    char ch1[] = {'a', 'b', 'c'};	// 数组长度为3
    char ch2[] = "abc";			   // 数组长度为4 多一个\0, 
    
  • 如果在定义数组时,指定了数组长度并对其初始化,凡是空的元素,int型初始化为0,char型初始化为‘\0’,指针型初始化为NULL。

字符串的末尾必须是\0结束

一维数组的操作

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

int size = siezof(arr)/size(arr[0]);	// 求数组长度 size = 40/ 4 = 10

二维数组的创建和初始化

  • 二维数组的创建

    int arr[3][4];
    char arr[10][20];
    double arr[3][4];
    
  • 二维数组的初始化

    int arr[3][4] = {1,2,3,4, 2,3,4,5, 6,7,8,9};
    int arr[3][4] = {1,2,3,4,5};		// 先 按行按行 的填满,剩余不够的用0填充
    int arr[3][4] = {{1,2},{3,4},{5}};	 // 这样 1,2就在第一行,3,4就在第二行,5在第一行,每一行不够的用0补充
    int arr[][4] = {{1,2,3,4},{2,3}};	 // 二维数组初始化时,可以省略第一维(行),但是不能省略二维
    
  • 三维数组也是只能省略第一维,二三维不能省略

二维数组的操作

  • 可以把二维数组看作是n个一维数组(也就是存放一维数组的数组)

    int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,1,2,3}};
    arr[0]就是第一行的数组的数组名
    arr[1]就是第二行的数组的数组名
    
    int line = sizeof(arr)/sizeof(arr[0]);			// 行数
    int column = sizeof(arr[0])/sizeof(arr[0][0]);   // 列数
    

数组作为函数参数

  • 数组名在传递给函数参数时,总是被视为指针

  • 数组传参的时候,形参有两种写法,无论哪种写法,实际上都是传入的数组首地址,数组本身没有被复制(所以在函数内部,不能求解参数为数组的数组长度

    void BubbleSort(int arr[], int size){...}  // 正确
    
    // 正确,可以把数组型形式参数声明为指针,声明arr是数组和声明arr是指针对编译器来说是完全一样的(仅对形式参数而言)
    void BubbleSort(int * arr, int size){...}  
    
    void BubbleSort(int arr[]){
        int size = sizeof(arr)/sizeof(arr[0]); // 错误,sizeof(arr)实际上是求一个指针变量的大小,这在32位机或64位机器中都是一个定值
        ...
    }
    
    // 写法1 数组(更方便理解)
    void func(int arr[], int size){...}
    
    // 写法2 指针
    void func(int* arr, int size){...}
    
  • 如果指定一维数组形式参数的长度,编译器实际上会忽略长度值,因为编译器不会检查数组的长度是否为指定的长度

    double average(int arr[10], int size){...}  // 这里的10是没有必要的	
    
  • 数组名到底是什么:

    数组名大多数情况下表示首元素的地址,但是有两个例外:

    1. sizeof(数组名),这里的数组名表示的是整个数组,计算的是整个数组的大小,单位是字节

       	2. `&数组名`,这种情况下的数组名表示的是**整个数组**,取出的是整个数组的地址
      
    int arr[10];
    
    printf("%p\n", arr);		// 首元素的地址 00000025FFB2F9D8
    printf("%p\n", arr + 1);	//			  00000025FFB2F9DC	=  00000025FFB2F9D8 + 4		
    
    printf("%p\n", &arr[0]);	// 首元素的地址 00000025FFB2F9D8
    printf("%p\n", &arr[0] + 1);// 			  00000025FFB2F9DC  =  00000025FFB2F9D8 + 4	
    
    printf("%p\n", &arr);		// 数组的地址  00000025FFB2F9D8
    printf("%p\n", &arr + 1);	// 			 00000025FFB2FA00 =  00000025FFB2F9D8 + 0x28(十进制:40),数组地址 + 1 直接跳过整个数组
    
    printf("%d]n", sizeof(arr));// 40
    
  • 二维数组的数组名理解:

    二维数组的数组名也表示首元素的地址(不是arr[0][0]的地址,而是arr[0]的地址,也就是第一行的地址)

    此时把二维数组想象成一维数组,一行就是二维数组的一个元素,所以二维数组名表示的是第一行元素的地址,而不是第一个元素的地址

    int arr[3][4];
    
    // sizeof(arr)是整个数组的大小
    // sizeof(arr[0])是第一行元素的大小
    // sizeof(arr[0][0])是第一个元素的大小
    int line = sizeof(arr)/sizeof(arr[0]);			// 行数
    int column = sizeof(arr[0])/sizeof(arr[0][0]);   // 列数
    
  • 如果函数的形式参数是多维数组,声明参数时只能省略第一维的长度,如:

    int sum(int a[][LEN], inr n){...}
    

C99 中的变长数组

  • 变长数组不是说数组的长度可变,而是数组的长度可以通过变量来指定

    int n = 10;
    char arr[n]; // 这在VS的编译器中是不允许的,仅在C99的编译器中支持
    
  • 变长数组的长度是在程序执行时计算的,而不是在程序编译时计算的

  • 变长数组有两个限制

    1. 变长数组没有静态存储期限,不可以用const修饰
    2. 变长数组不能有初始化式
    3. 不允许goto语句绕过变长数组的声明:因为遇到变长数组声明时,通常就该给变长数组分配空间了,如果跳过了其声明,可能导致对未分配空间的数组中的元素的访问
  • 变长数组常见于除main()函数之外的其他函数

数组的复制

  • 方法一,通过循环实现
  • 方法二,通过<string.h>头文件中的memcpy内存复制函数实现:memcpy(a,b,sizeof(a)),这个速度比循环更快

数组的类型

  • 数组也是有类型的,如:

    int arr[10] = { 0 };  // 类型是 int[10] 中间的数字10不能少
    
    printf("%d\n", sizeof(arr));		// 40
    printf("%d\n", sizeof(int [10]));	// 40
    

在这里插入图片描述

函数

  • 库函数:IO函数,字符串操作函数、字符操作函数、内存操作函数、日期、时间操作函数、数学函数等

  • 函数设计应该追求高内聚底耦合:即让一个函数尽量把要实现的功能都放在自己内部,降低和其他函数的关联,让函数的功能尽可能的单一

  • 函数应该尽可能少的使用全局变量

  • 函数设计时不应给过多的参数

  • 设计函数时,尽量做到谁申请资源就由谁来释放

  • 省略函数的返回类型时,C89默认的返回值类型是int

  • 如果函数的返回值类型很长,可以单独把它放在函数名上边一行,如:

    unsigned long ing
    average(double a, double b){
    	。。。
    }
    
  • 习惯:如果函数没有参数,也要加上void

    int func(void){...}
    
  • 必须在函数调用之前,定义该函数,或者声明该函数的函数原型

  • 函数原型不需要说明函数形式参数的名字,只需显示他们的类型,如:

    double average(double, double);  // 但是不推荐省略形参名字
    

实参和形参

  • 形式参数当函数调用完成后就自动销毁了
  • 当实参传递给形参后,形参是实参的临时拷贝,对形参的修改不能改变实参
void swap_1(int a, int b) {   // 无效
	int z = a;
	a = b;
	b = z;
}

void swap_2(int* a, int* b) {  // 有效
	int z = *a;
	*a = *b;
	*b = z;
}
// 因为实参传给形参的是地址,形参是对地址的拷贝,因次对可以对实参地址里的内容进行修改,因为都是一个地址
  • 数组传参(实际上传递的是数组首元素的地址,而不是整个数组)
void test(int a[]){...}

int main(void){
    ... 
    int a[] = {...};
    test(a);   // 传入的不是整个数组的空间,而是传入的数组首地址
    ...
    return 0;
}

// 因次在test函数内部求解数组的长度是不行的
void test(int a[]){  // 本质上形式参数int a[] 是一个指针变量
    
    int size = sizeof(a)/sizeof(a[0]);  // 这里sizeof(a)就是一个指针变量的大小,这个是固定的4个字节(32位机)
} 

因此,无法在函数内部计算形式参数数组的长度

  • 函数调用

    • 传值调用:函数的形参和实参分别占用不用的地址,对形参的修改不会影响实参。这种值传递的方式,可以把形参当作是函数内的变量来使用,这样可以真正减少需要的变量的数量

      void swap_1(int a, int b) {   
      	int z = a;
      	a = b;
      	b = z;
      }
      
    • 传址调用:传入的实参的内存地址,这种传参方式可以真正建立起函数内部和外部之间的联系,也就是函数内部可以直接操作函数外部的变量

      void swap_2(int* a, int* b) { 
      	int z = *a;
      	*a = *b;
      	*b = z;
      }
      
  • 函数不写返回值的时候,默认返回类型是int(不推荐这种写法)

    ADD(int x, int y){  // 默认返回类型是Int
    	return x + y;
    }
    
  • 函数写了返回值后,有些编译器慧返回函数中执行过程中最后一条指令的结果

    int Add(int x, int y){
    	printf(x+y);
    }
    
  • 函数没有定义参数时,如果没有写void,是可以传入参数的,但是没有意义

    void test(){
        printf("你好\n");
    }
    ...
    test(100);  // 这样写可以,但是不推荐
    
    如果要明确的不能传入参数,必须写上void
    
  • C语言允许调用函数时,实参和形参的类型不匹配,按调用前是否遇到函数原型分为两种情况:

      1. 编译器在调用前遇到函数原型。此时每个实际参数的类型被隐式地转换成函数原型中给出的形参的类型。
      2. 编译器在调用前没有遇到函数原型。编译器执行默认的实际参数提升,如:把float类型转换成double类型。
  • 形式参数和局部变量具有相同的性质,即自动存储期限和块作用域。事实上,形参和局部变量唯一的真正的区别是,在每次调用时,对形式参数自动进行初始化(在调用过程中,通过赋值,获得相应实际参数的值)

关于main函数的参数

  • 明确的说明main函数不需要参数:

    int main(void){...}
    
  •   int main(int argc, char* argv[], char* envp[]){...}
    

函数的定义或声明

  • 程序是顺序执行的,如果某个自定义的函数放在了使用的后面,编译器会无法通过,因此需要在使用之前声明一下这个函数,告诉编译器

    int Add(int x, int y);
    
    int main(void){
        
        ...
        ADD(x,y);
            
        return 0;
    }
    
    int Add(int x, int y){
        return x + y;
    }
    
  • 函数声明:

    • 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体存不存在,函数声明决定不了,也就是这个函数可以不存在(假声明)

    • 函数的声明一般出现在函数的使用之前。要满足先声明后使用

    • 函数的声明一般要放在头文件中。(模块化编程),而单独把函数的实现放在一个源文件中,把它单独编译成一个库(静态库.lib 动态库.dll),就可以隐藏函数的实现细节,保护自己的代码,别人使用的时候只用函数的接口(头文件给对方),看不到函数的代码实现。

      // 在项目中添加静态库 add.lib
      
      // 这是导入静态库的一种方法
      #pragma comment(lib, "add.lib") 
      int main(void){
          ...
          return 0;
      }
      

函数递归和迭代

  • 递归即一个过程或函数在其定义或说明中直接或间接调用自身的一种方法

  • 递归的两个必要条件:

    • 存在限制条件,即递归出口,当满足这个限制条件的时候,递归便不再继续
    • 每次递归调用之后应该越来越接近于这个条件,否则递归仍然无法结束
  • 循环属于迭代,但迭代不仅仅包含循环

  • 如何选择递归和迭代:

    如果用递归解决代码非常简单而且没有错误,可以选择递归,但如果效率太差,应选择迭代

    递归的层次如果太深可能会出现栈溢出的情况,因为递归包含大量重复的计算

  • 可以使用static对象(在堆中)代替nonstatic局部对象(在栈中),这样可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归状态的中间状态,并且可以为各个调用层所访问

typedef struct{
    char id[10];
    char name[50];
    int score;
}Student;

#define MAX_STUDENT 100
int numStudent = 0;
a. int findStudent(char* id);		// 通过学号查找学生信息
b. void addStudent();			   // 添加学生信息,包括姓名,成绩,学号
c. void deleteStudent(char* id);	// 通过学号删除学生信息
d. void printStudnet();			   // 打印学生信息 

指针

指针就是地址,指针变量就是存储地址的变量

  • int* p = a,说p指向a,此时*p就是a的别名,他们俩是等价的 *p,*&a,a 三者是等价的
  • * 称为解引用操作符,又叫间接寻址运算符
  • & 和 * 运算符相遇时,可以抵消

const修饰的指针

  1. const int* p

    • 表示,p是指向“常整数”的指针,const保护p指向的对象,因此,*p不能被改变。*p=20是错误的
    void func(const int* p){
    	int j;
    	*p = 20;  // 错误
    	p = &j;	  // 正确,但不会对函数外部有任何影响
    }
    
  2. int* const p

    • 表示p是一个常指针,const可以保护 p 本身, const离p更近就保护p
    void func(int* const p){
    	int j;
        *p = 20;  // 正确
    	p = &j;	  // 错误
    }
    
  3. const int* const p

    • 表示,同时保护p和p指向的对象,此时都不能修改
    void func(int* const p){
    	int j;
        *p = 20;  // 错误
    	p = &j;	  // 错误
    }
    

指针算术运算(地址算术运算)

  • C语言支持三种格式的指针算术运算

    1. 指针加上整数(如果p指向a[i],那么 p + j 指向a[i + j])
    2. 指针减少整数(如果p指向a[i],那么 p - j 指向a[i - j])
    3. 两个指针相减(如果p指向a[i],q指向a[j],那么 p - q 就等于 i - j),只有两个指针指向同一个数组时,他们相减才有意义
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *p, *q, i;
    
    p = &a[2];
    q = p + 3;  // 指针加上一个整数,q = &a[5]
    P += 6;     // 指针加上一个整数,p = &a[8]
    
    q = p - 3;  // 指针减少一个整数,q = &a[5]
    p -= 6;	    // 指针减少一个整数,p = &a[2]
    
    p = &a[5];
    q = &a[3];
    i = p - q; // i = 5 - 3 = 2
    i = q - p; // i = 3 - 5 = -2
    

指针比较

可以对指针用关系运算符(<, <=, >, >=)和判等运算符(==, !=)进行比较,但,当且仅当,两个指针指向同一个数组时,指针的比较才有意义,其比较的结果依赖于数组中两个元素的相对位置。

p = &a[5];
q = &a[1];

p <= q;  // 结果是0
p >= q;  // 结果是1

*运算符和++运算符

在这里插入图片描述

后缀++的优先级比*更高,

*p++ 和 *(p++)  等价
(*p)++
*++p 和 *(++p)  等价
++*p 和 ++(*p)  等价

用数组名作指针

可以使用数组的名字作为指向数组第一个元素的指针

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

*a = 6;  	 // 相当于a[0] = 6
*(a+1) = 7;  // 相当于a[1] = 7
  • a + i 就等于 &a[i] , *(a+i) 就等于 a[i]

  • 把数组名当作指针时,不能给数组名赋新的值

    int a[10];
    a++;  // 这样写是错误的
    
    可以把a赋值给一个指针变量,然后修改该指针变量
    p = a;
    p++; // 这是可以的
    
    int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *p, *q, i;
    
    p = &a[2];
    q = p + 3;  // 指针加上一个整数,q = &a[5]
    P += 6;     // 指针加上一个整数,p = &a[8]
    
    q = p - 3;  // 指针减少一个整数,q = &a[5]
    p -= 6;	    // 指针减少一个整数,p = &a[2]
    
    p = &a[5];
    q = &a[3];
    i = p - q; // i = 5 - 3 = 2
    i = q - p; // i = 3 - 5 = -2
    

http://www.kler.cn/a/551395.html

相关文章:

  • 信息收集-Web应用JS架构URL提取数据匹配Fuzz接口WebPack分析自动化
  • Vue.js 组件开发:构建可复用的 UI 组件
  • Spring如何去解决循环依赖问题的?
  • 游戏数据中枢系统的架构设计与实现——以GameDataOrchestrator为核心的模块化数据管理体系
  • 基于IOCP模型的服务器接待流程设计与实现——以奶茶店运营为隐喻
  • 浅谈Spring Boot MQTT功能并实现手动连接操作
  • 500. 键盘行 771. 宝石与石头 简单 find接口的使用
  • 机械学习基础-6.更多分类-数据建模与机械智能课程自留
  • 基于kafka、celery的日志收集报警项目
  • NCV4275CDT50RKG 车规级LDO线性电压调节器芯片——专为新能源汽车设计的高可靠性电源解决方案
  • 网络安全特性
  • 【CSS进阶】常见的页面自适应的方法
  • 2.17日学习总结
  • 解决 MyBatis Plus 在 PostgreSQL 中 BigDecimal 精度丢失的问题
  • 什么是 大语言模型中Kernel优化
  • 在不使用 Spring Security 的情况下获取用户登录参数
  • 基于Cilium的全栈eBPF服务网格:颠覆传统Sidecar模式的云原生通信革命
  • Windows程序设计25:MFC中常用窗口类及关系
  • Linux-ISCSI
  • spring boot和spring cloud的关系