C语言总结

总结C语言中经常忘记、易混淆的知识,后续遇到其他问题再补充。若有错误,欢迎指正。

数据类型

C语言规定了各种类型的最小大小,但没有明确规定各种类型的具体大小

  • 基本类型 —— short、int、long、float、double、char
  • 构造类型 —— [ ]、struct、union、enum
  • 指针类型 —— *
  • 空类型 —— void

变量

程序运行期间可以发生改变的量,本质是一块内存空间。

声明

  • 不开辟内存空间的声明:extern int a;
  • 开辟内存空间的声明(定义):int a;

定义

  • 开辟内存空间并赋值:int a = 0;

区别

  • 声明可以有多次,定义只能有一次
  • 声明可能不需要开辟空间,定义一定开辟内存空间
  • 在函数体内不能初始化被extern标记的变量
extern int a;	//没有分配内存空间
int b;			//分配内存空间0x0078FCDC

常量

常量也称为字面量,是固定值,在程序运行期间不改变,可以是任何基本数据类型。

整型常量
  • 二进制不能表示整型常量,只有八进制、十进制、十六进制3种表示方式。

  • 前缀指定用何种进制解释常量

  • 后缀限定常量的类型(不区分大小写)

    //前缀
    int a = 12;	 //默认十进制a=12
    int b = 012; //八进制b=10
    int c = 0x12;//十六进制c=18
    
    //后缀 整型常量默认 int
    1234;	//int
    1234u;	//u -> unsigned
    1234l;	//l -> long
    1234lu;	//lu、ul -> unsigned long
    1234ll;	//ll  -> long long
    1234llu;//llu -> unsigned long long
    
浮点型常量

只能使用十进制,有两种形式:

  • 小数形式:-1.23

  • 指数形式:1.23e+2 = 1.23*10^2

  • 后缀限定浮点常量的类型

    //浮点常量默认 double
    1234.0;	//浮点常量默认 double
    1234.0f;//f 指定为 float
    1234.0l;//l 指定为 long double
    
#define & const
  • #define定义的常量不占内存,编译后就不存在,效果相当于直接替换

    #include <stdio.h>
    #define PI 3
    int main() {
    	printf("%d\n", PI * 2);	// <=> 3 * 2 = 6
    	return 0;
    }
    
  • const 定义的常量的值不能被直接修改,不能被赋值,但可以使用指针修改const常量的值

    const int a = 10;
    //a = 6;	//表达式必须是可以修改的左值,无法通过编译
    
  • #define & const 区别

    #defineconst
    起作用阶段预编译时作用编译、运行时作用
    作用方式直接替换类型检查
    存储空间符号常量的名字不分配存储空间占用存储空间,变量值不改变
    调试无法调试可调试

格式化输入输出

printf()

格式化输出函数,向 stdout 按规定格式输出信息。在输出时:

  • 普通字符:按原样复制到标准输出
  • 转换说明:按转换说明符控制 printf 要输出数据的类型、宽度、精度等。
  • 返回值:要显示的字符串的长度

转换说明组成:%[标记][最小宽度].[精度][长度修饰符][转换说明]

  • 标记
  • [长度修饰符]
    常见的长度修饰符有:h(short)、l(long)、ll(long long)、z(size_t), 如果长度修饰符与转换说明不匹配则会引起未定义的行为。
  • [转换说明]
#include <stdio.h>
int main() {
	int x = 6;
	float f = 11.234;
	printf("x = |%d|\n", x);
	printf("x = |%-4d|\n", x);	//向左对齐 |6   |
	printf("x = |%4d|\n", x);	//向右对齐 |   6|
	printf("x = |%04d|\n", x);	//使用0补齐|0006|

	printf("f = |%f|\n", f);	//							|11.234000|
	printf("f = |%10f|\n", f);	//宽10位,默认小数点后6位		| 11.234000|
	printf("f = |%-10.2f|\n", f);//左对齐,宽10位,小数点后2位|11.23     |
	printf("f = |%10.0f|\n", f);//右对齐,无小数 			   |        11|

	//printf返回值 => 显示字符的个数
	int pret = printf("Hello world.\n");
	printf("printf返回值:%d\n", pret);

	return 0;
}
scanf()

格式输入函数,从进程空间的标准输入缓冲区 stdin 中读取字符与转换说明进行匹配,该过程中若

  • 匹配成功:将读取的字符、数字从缓冲区删掉,并继续向后匹配直到格式串匹配完,返回匹配成功的个数。
  • 匹配失败、到达文件末尾、发生读错误:立即返回**EOF**,它在 stdio.h 中的定义是 #define EOF (-1)

格式组成:%[*][域宽][长度修饰符][转换说明]

  • 读取基本类型的变量:&变量名;读取字符串、指针变量不使用&。

    C语言中的字符串由**字符数组+\0**组成,而数组名、指针变量名本身就是地址,因此使用scanf()函数时,不用在它们前面加上 &

  • [*]: 表示忽略读入的数据

    #include <stdio.h>
    int main() {
    	// * 忽略读入的数据
    	int a[3] = { 0 };	//初始全0
    	scanf("%d %*d %d", &a[0], &a[1], &a[2]);	//%*d被忽略 若输入10 9 8,9会被忽略
    	printf("a[0]=%d,a[1]=%d,a[2]=%d", a[0], a[1], a[2]);//a[0]=10,a[1]=8,a[2]=0
    	return 0;
    }
    
  • [域宽]:非零十进制整数,指定宽度(最多读取的字符数),分隔字符串。
    eg:输入一个字符串,按长度为8拆分每个输入字符串(每个字符串长度小于等于100)(牛客网:华为机试)

    // 输入:abc
    // 输出:abc00000
    #include <stdio.h>
    #include <string.h>
    int main(){
        char str[100];
        while(scanf("%8s", str) != EOF)  {
            int len = 8 - strlen(str);
            printf("%s", str);
            for(int i = 0; i < len; i++)
                printf("0");
            printf("\n");
        }
    }
    
  • [长度修饰符]:同printf()。

  • [转换说明]

    ​ 匹配的规则(同printf()的转换说明一样),按规则将数据转换为对应的二进制数据。发生匹配时,普通字符一对一精确匹配,空白字符(空格、换行符、制表符等)可以匹配任意多个。其中

    • %s :读取字符串时遇到 \0、\n即停止。

    • %c读取空白字符(空格、\n、制表符等)在内的所有字符,所以混合输入时, %c 前加一个空格。匹配时空格负责匹配读入的空白字符,而%c就可以匹配其他字符。eg: %s%c 同时使用,在 %c 前加空格,让空格去匹配字符串最后的\0**,%c则匹配其他普通字符。

      #include <stdio.h>
      int main(){
          char string[20], t;
      	scanf("%s %c", string, &t);				//输入:test hh
      	printf("string:%s t:%c", string, t);	//string:test t:h
      	return 0;
      }
      
  • 其他

    scanf()行缓冲,当行缓冲区空时阻塞;非空时一直从行缓冲区中读取并匹配字符。

    #include <stdio.h>
    
    int main(){
    	int i;
    	char c;
    
    	scanf("%d", &i);
    	printf("i=%d\n", i);
        
    	scanf("%d", &c);
    	printf("c=%d\n", c); //按%d从缓冲区匹配,不会匹配换行符
    	scanf("%c", &c);	 //上一个printf取走一个整型数后,还有\n在缓冲区中(非空),%c会将\n匹配给变量c
    	printf("c=%c\n", c); //变量c是\n,加上本行\n共输出两个换行符
    
    	return 0;
    }
    

    运行结果:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WRFsUJLL-1679559360223)(./image/scanf.png)]

存储类别

C语言中,每一个变量和函数都有两个属性:数据类型数据存储类别。存储类别定义了变量、函数的范围、生命周期,有以下几类:

  • auto:所有局部变量默认的存储类别,只能用在函数内,修饰局部变量。没有使用static声明的、动态分配存储空间的变量都属于该类。

  • static:把局部变量分配在静态存储区,限制全局变量、函数作用域为该文件

  • register:将变量存储在寄存器中。

    • 当所有寄存器被占用时不能被分配到寄存器中
    • 不能对分配到寄存器中的变量使用指针、&(因为寄存器没有内存地址)。
  • extern:两个或多个文件共享全局变量或函数时使用,拓展变量的作用域。

static & auto
  • static静态存储区分配内存单元; auto动态存储区分配内存单元
  • static 编译时赋初值,且只赋一次初值;auto函数调用时赋值,调用一次赋一次值
  • static 定义时若未赋值,编译时赋值 0、\0auto 若未赋值,值不确定
#include <stdio.h>

int main() {
	int a;				//VS2022:cc cc cc cc
	static int b1;		//默认赋初值0
	static int b2=6;	//6
	register int c=0;	//0
	printf("%d %d %d", b1, b2, c);//0 6 0
	return 0;
}

运算

重点关注运算符的两个属性:优先级、结合性(从左向右结合、从右向左结合)

优先级

简单记忆:算数运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符 。详细优先级见C语言运算符优先级图

sizeof
  • sizeof() 是一个运算符,而非函数,在编译阶段就获得结果。

  • 计算某一类型占内存空间的大小(以字节为单位),返回size_t类型的值(无符号整型,使用“zd”、“%u”等转换说明)

模运算 %
  • 只能对整数取模,使用printf打印 % 时转换说明是 %%

  • 若对负数取模,取模的结果符号同第一个运算对象的符号,eg : -11 % 5 = -1 11 % -5 = 1

    #include <stdio.h>
    
    int main() {
    	printf("%d %% %d = %d\n", 4, 3, 4%3);	//4 % 3 = 1
    	printf("%d %% %d = %d\n", 4, -3, 4%-3);	//4 % -3 = 1
    	printf("%d %% %d = %d\n", -4, 3, -4%3);	//-4 % 3 = -1
    	printf("%d %% %d = %d\n", -4, -3, -4%-3);//-4 % -3 = -1
    	return 0;
    }
    
  • 判断整数奇偶性(%、位运算&

    #include <stdio.h>
    #include <stdbool.h>
    
    bool is_odd(int x) {
    	return x % 1 != 0;
    }
    
    int main() {
    	// % 判断奇偶性
    	if (is_odd(6))
    		printf("odd\n");
    	else
    		printf("even\n");
    
    	return 0;
    }
    
赋值运算
  • 赋值过程可能发生隐式类型转换

  • = 从右向左结合,eg:i = j = k = 2;等价于i = ( j = (k = 2));

    #include <stdio.h>
    
    int main() {
    	float f;
    	int i;
    	f = i = 6.66f;
    	printf("i=%d f=%f", i, f);//i=6 f=6.000000
    
    	return 0;
    }
    
自增(减)运算
  • ++-- 只能作用于变量,包括:char、int、float

  • i++ :表达式的值为 i++i :表达式的值为 i+1,都会产生副作用 —— i 自增

    #include <stdio.h>
    int main() {
    	//后++(后-- 一样)
    	int i = 6;
    	printf("i=%d\n", i++);//i++ 的值为i  => 6
    	printf("i=%d\n", i);  //i++ 的副作用 => i自增,i=7
    	//前-- (前++ 一样)
    	printf("i=%d\n", --i);//--i的值为i-1,产生副作用 = > i自减,i=6
    	printf("i=%d\n", i);	 
    
    	return 0;
    }
    
  • 混合运算时,++i 和 i++ 自增顺序不同:++i先自增再赋值i++先赋值再自增。eg:

    q = 2 * ++i		// i 先自增再乘以2,再赋值给q
    q = 2 * i++		// i 先乘以2再自增,再赋值给q
    
  • 错误用法

    • i=i++;
    • j=(++i)+(i++);
    • a[i]=b[i++];
类型转换

类型级别从低到高:int、unsigned int、long、unsigned long、long long、unsigned long long、float、double、long double

  • char、short => int => long => long long => float => double
  • signed => unsigned
  • 作为函数参数传递时,char 和 short 转为 int(整数提升),float 转为 double
  • 强制转换、隐式转换,都只是为了本次运算的需要而对变量的数据长度进行的临时性转换,而不改变数据声明时对该变量定义的类型
隐式类型转换
  • 编译器自动转换

  • signedunsigned 不要混合运算,会出现意料之外的结果(编译器会自动将signed转换为unsigned)。

    #include <stdio.h>
    int main() {
    	unsigned int a = 100;
    	int b = -1;			
    	if (a > b)	//b发生类型转换,由signed => unsigned -1补码1111 1111 (255)
    		printf("a > b");
    	else 
    		printf("a < b");
    	return 0;
    }
    
强制类型转换
  • 强制类型转换只转换紧跟的一个值,要使用 () 才能转换表达式的值。

    #include <stdio.h>
    
    int main(){
    	int a = 5, b = 2;
    	float c1 = a / b;	//2.00000000
    	float c2 = (float)a / b + b;	//4.50000000
    	float c3 = (float)(a / b + b);	//4.00000000
    
    	return 0;
    }
    
  • 计算浮点数小数部分

    double d = 3.14, fraction;
    fraction = d - (int)d;
    
  • 避免溢出

    #include <stdio.h>
    
    int main() {
    	long long a = 10 * 10 * 10;
    	long long b = 1000 * 1000 * 1000 * 1000;//1000默认是int型,则1000*1000*1000*1000也是int型,但是发生了溢出
    											//赋值给b的是一个溢出后的int值
        //避免溢出
    	long long c = (long long)1000 * 1000 * 1000 * 1000;//强制类型转换后不会发生溢出
    	printf("b/a=%lld\n", b/a);	//-727379  b溢出,计算错误
    	printf("c/a=%lld\n", c/a);	//1000000000 强制转换后b不再溢出
    
    	return 0;
    }
    
整数运算

C语言中两个整数相除或取余其结果依旧为整数,当两个整型数相除结果为小数时,先强制类型转换再运算才可以输出小数。eg:

#include <stdio.h>

int main(){
	int i = 5;
	float j = i / 2;			//2.000000
    float k = float(i) / 2; 	//2.500000

	printf("%f %f\n", j, k);
    return 0;
}
浮点数比较

通过两个数相减结果是否为零判断两个浮点数是否相等,不能使用 ==

​ 浮点数按照IEEE754标准存储,由于浮点数的精度问题,有些浮点数只是一个近似值(eg: 23.45 的值为 23.4500008)因此要判断两个浮点数是否相等,需要将两个浮点数作差,如果差值在某个范围内则相等,否则不相等,这个范围要根据浮点数的精度判断。如下实例中由IEEE754标准float精度为7位,234.56已使用3位有效精度,所以和0.0001、-0.0001比较,只要在该范围内,即是相等。

#include <stdio.h>

int main(){
	float f = 234.56;

    //错误方法,输出f is not equal to 234.56
	if (f == 234.56)
		printf("f is equal to 234.56\n");
	else
		printf("f is not equal to 234.56\n");

    //正确方法,输出f is equal to 234.56
    if (f - 234.56 > -0.0001 && f - 234.56 < 0.0001)
		printf("f is equal to 234.56\n");
	else
		printf("f is not equal to 234.56\n");

    return 0;
}
短路运算
  • 可以使用短路运算替代 if

  • expression1 && expression2 : 当expression1的值为0时与任何表达式相与结果都为0,这时不会再执行expression2。

    #include <stdio.h>
    int main(){
    	int i = 0;
    	i && printf("Error!");	//不会打印Error! <=>if(i) printf("Error!");
        
        return 0;
    }
    
  • expression1 || expression2 运算时 :当expression1的值为1时与任何表达式相与结果都为1,这时不会再执行expression2’

    #include <stdio.h>
    int main(){
    	int i = 1;
    	i || printf("Error!");	//不会打印Error!
        
        return0;
    }
    
  • 当存在短路运算时,优先级最高的 ()也会被短路

    #include <stdio.h>
    int main(){
    	int i = 0, j = 4;
        
    	i && (i = j);		//()被短路,不起作用
    	printf("%d\n", i);	//0
    
    	i=1 || (i = j);		//()被短路,不起作用
    	printf("%d\n", i);	//1
    
    	return 0;
    }
    
位运算
  • 左移 <<

    • 左移相当于×2。由于CPU中没有加法器,有移位器,所以移位运算比乘法运算效率高。

    • 有符号正数左移可能变为负数,有符号负数左移可能变为正数。为了代码的移植性,最好不要对有符号数进行移位运算。

    • **使用 <<开辟空间 **,eg : 使用 malloc(1<<30) (1G = 230)申请1G内存空间。

      #include <stdio.h>
      
      int main() {
          short i1 = 0x7385, i2 = 0x8051;
          short j1 = i1 << 1, j2 = i2 << 1;
      
          //i1 = 7385 (29573)  i1<<1 = -6390
          printf("i1 = %X \(%d\)	i1<<1 = %d\n", i1, i1, j1);
      
          //i2 = FFFF8051 (-32687)  i2<<1 = 162
          printf("i2 = %X \(%d\)	i2<<1 = %d\n", i2, i2, j2);
      
          return 0;
      }
      
  • 右移 >>

    #include <stdio.h>
    int main(){
        int i1 = 7, i2 = -7;
        printf("%d %d", i1>>1, i2>>1);	// 3 -4
        return 0;
    }
    
  • 移位运算不改变原来变量的值 ,但移位赋值可以改变变量的值。

    #include <stdio.h>
    
    int main(){
    	int i = 8;
    	int j = 6;
    	printf("i >> 1 = %d, i = %d\n", i>>1, i);	//i >> 1 = 4, i = 8
    	printf("j <<=2 = %d, j = %d\n", j <<= 2, j);//j <<=2 = 24,j = 24
    
    	return 0;
    }
    
  • & 判断奇偶性

    bool is_even(int x){
    	return x & 1 == 0;
    }
    
  • ^ 性质:a ^ 0 = aa ^ a = 0a ^ b = b ^ a(a^b)^c = a^(b^c)

    • 判断整数是否是2的次幂(2^n 的二进制表示只有一个1):

      //1.左移赋值判断
      bool isPowerOf2(unsigned int x){
      	unsigned int i = 1;
      	while (i<x){
      		i <<= 1;
      	}
      	return i == x;
      }
      
      //2.按位异或
      bool isPowerOf2(unsigned int n){
      	return (n ^ n) - 1 == 0;
      }
      
逗号 ,

连接多个表达式,有括号时值为最后一个表达式的值,没有括号时值为第一个表达式值。

#include <stdio.h>

int main() {
    int a=0,b,s=2,d=3;
    b=a,d+2;		//b=0
    a=12+(s+2,d+4); //a=19
    return 0;
}

控制流

循环边界

i ∈ [ 0 , n ) i\in \left [ 0,n \right ) i[0,n) i ∈ [ 1 , n ] i \in \left [ 1,n \right ] i[1,n] 循环次数相同,循环结束时的 i 不一样:

  • for(int i = 0; i < n; i++):循环 n 次,循环结束时 i == n;
  • for(int i = 1; i <=n; i++):循环 n 次,循环结束时 i == n+1;
  • for(int i = m; i < n; i++):循环 n - m 次,循环结束时 i == n ;
  • for(int i = m; i <=n; i++):循环 m - n + 1 次,循环结束时 i == n+1 。
break
  • 跳出 switchwhilefor 循环,当它们嵌套时,只能跳出包含 break 的最内层嵌套

  • 防止case穿透

continue
  • 跳转到循环体末尾,本次循环的剩余部分不再执行,准备开始下一次循环(并没有跳出当前循环)。

  • 用作占位符。

      while(getchar()!='\n');
          continue;
    
goto
  • 在同一个函数内跳转到相应标签语句

  • case 内跳出 while 循环、多层 while 嵌套跳出循环时只能使用 goto

    while(1){
        switch(){
            case 1: ;
    		......
        }
    }
    
    while(1){
        while(...){
            while(...)
                ...
        }
    }
    
  • 错误处理

    #include <stdio.h>
    
    int main(){
    	int count = 16;
    	while (1){
    		for (int i = 0; ; i++){
    			if(i>count)
    				goto error_handle;
    		}
    	}
    error_handle: printf("error_handle!");
    	return 0;
    }
    
switch-case
  • 表达式值必须是整数类型
  • case后必须是整数类型的常量表达式,不能有重复的标签
  • 效率、可读性比级联式 if...else 高,但是不如它常用。
case穿透

当某个 case语句中没有break时,程序会一直执行直到遇到 break。

#include <stdio.h>

int main() {
	int score;
	scanf("%d", &score);
	switch (score/10){	//表达式必须是整型、枚举类型
        case 10:	//90-100视为优秀,当score=100(case=10)时,由于没有break;会向下执行case9的语句,遇到break时跳出分支
        case 9:
            printf("优秀\n");
            break;
        case 8:
        case 7:
            printf("良好\n");
            break;
        case 6:
            printf("及格\n");
            break;

        default:
            printf("不及格\n");
            break;
	}

	return 0;
}

数组

  • 相同数据类型有序、连续存储在栈空间,初始化时就确定了大小。

  • 数组下标从0开始,若下标从1开始,则每一次寻址都会多一次减法运算

    • 下标从0开始:a[i] _address = base_address + i * type_size ;

    • 下标从1开始:a[i] _address = base_address + ( i - 1) * type_size

    舍弃a[0],从第二个元素a[1]开始存数组元素也是可以的,但是会浪费内存。在早期的时候计算机的计算、内存资源比较宝贵,故下标从0开始。

一维数组
  • 初始化

    int a[5] = {4, 5, 6, 7, 8}; //初始化所有元素
    int c[5] = {0};				//所有元素赋初值0
    int b[5] = {1,2,3};			//部分元素赋值,其余元素为0.数组b所有元素:1,2,3,0,0
    int e[5]; e[0]=2; e[2]=4;	//部分元素赋值,其余元素脏数据、随机数据
    int d[ ] = {1,2,3}; 		//编译器自动匹配数组大小、元素个数
    int f[ ] = {0}		//!!!!只有一个元素,值为0,错误用法
    

    不允许有以下情况:

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

    ​ 原因:数组元素大小确定之后,a[x]代表访问某个元素a[10] = {1,2,3}就是给第11个元素赋值1,2,3这显然是不可行的。除此之外,a[10]访问第11个元素会造成数组访问越界

  • 数组名a代表首元素a[0]的地址,一维数组名(一维数组首地址) ⇔ \Leftrightarrow 一维数组元素首地址。eg:int a[3];a == &a[0]

    #include <stdio.h>
    int main() {
    	int a[3];
    	printf("a=%x, &a[0]=%x\n", a, &a[0]);	//b3fd84 b3fd84
    	return 0;
    }
    
  • sizeof(array)/sizeof(array[0]) :计算数组元素个数。当**一维数组做函数形参时,不能这样用!**

    #include <stdio.h>
    
    //宏函数求数组大小
    #define SIZE(a) (sizeof(a)/sizeof(a[0]))
    
    int main(){
    	int arr[10]={0};
    	printf("SIZE(arr)=%d", SIZE(arr));
        return 0;
    }
    
二维数组
  • C语言中二维数组以行优先方式存储,存满一行再往下存。

  • 初始化:二维数组的一维大小可以省略,不能省略第二维的大小。eg:int array[][10]int array[10][10]

    int a[3][3] = { {1,2,3},{5,6,7},{8,9,0} };
    int b[3][3] = { {1},{2,3},{4,5,6} };
    int c[3][3] = { 1,2,3,4,5,6 };	//编译器按行优先自动分配,其余元素为0
    int d[ ][3] = { 1,2,4,6,8 };	//编译器按行优先自动分配,其余元素不确定(随机值、脏数据)
    int e[3][3] = {0}				//全0二维数组
    
  • 二维数组的数组名(二维数组首地址) ⇔ \Leftrightarrow 二维数组的首元素地址 ⇔ \Leftrightarrow 数组的首行地址。eg:int a[3][3],a == &a[0][0] == a[0]

    #include <stdio.h>
    int main() {
    	int a[3][3];
    	printf("a=%x, a[0]=%x, &a[0][0]=%x\n", a, a[0], &a[0][0]);//a=fbf854, a[0]=fbf854, &a[0][0]=fbf854
    	return 0;
    }
    
  • 求二维数组行数、列数

    #include <stdio.h>
    #define LINE(matrix) (sizeof(matrix)/sizeof(matrix[0]))	//二维数组行数
    #define ROW(matrix) (sizeof(matrix[0])/sizeof(matrix[0][0]))//二维数组列数
    
    int main() {
    	int a[][3] = {{0,1,2},{3,4,5},{6,7,8}};
    	printf("%d line, %d row\n", LINE(a), ROW(a));
    
    	return 0;
    }
    
常量数组
  • 数组元素不能被改变,存放静态数据。

    const int arr[4] = { 1,2,3 };
    //arr[2] = 6;	//表达式必须是可修改的左值
    
访问越界

​ 数组越界后会把数据放到未知区域,该区域原来的值被覆盖。如果该区域是系统的某个重要区域,可能因为数据被修改而造成系统崩溃。编译器并不会检查程序对数组下标的引用是否在数组的合法范围内,好的做法是:

  • 下标值是通过已知正确的值计算得来的,可以不检查。
  • 由用户输入的数据产生的下标值,在使用之前必须进行检查,确保在有效范围内。
#include <stdio.h>

void print(int b[], int len){
	int i = 0;
	for (i = 0; i < len; i++)
		printf("a[%d]:%d \n", i, b[i]);
}

int main(){
	int j = 10;
	int a[5] = { 1,2,3,4,5 };
	a[5] = 8;	//此时数组已经越界
	a[6] = 7;
	a[7] = 4;
	print(a,5);	//a[5]所有元素:1 2 3 4 5
	printf("j=%d", j);	//j=4 j本应该是10,但由于数组越界导致j=4

	return 0;
}

结果:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c89py0Xd-1679559360224)(E:/Users/%E5%9B%BE%E7%89%87/Typora/%E6%95%B0%E7%BB%84%E8%B6%8A%E7%95%8C.png)]
​ 上述程序在 a[5]=8 处已经发生数组越界。因为微软的编译器设计了不同变量之间有8个字节的保护空间(Mac和Linux没有),为了看数组越界会修改变量值的效果,17、18行继续赋值。由运行结果可以看到,数组越界导致原来的 j 被越界的 a[7] 覆盖

字符串
  • 字符数组:必须使字符数组大小比字符个数多,用 \0 作为结束的标志。

    char a[10] = {'h','e','l','l','o','\0'};
    
  • 字符串

    char a[8]="Hello";
    char b[]="Hello!";	//编译器自动添加‘\0’
    
    • 字符串常量:用 "" 括起来的字符序列,eg:"hello world",编译器会把相邻(以空白字符分割)的字符串常量拼接成一个字符串常量。

      printf("hello"
             " world"
             "!");	//hello world!
      
    • 字符串变量:C语言没有专门的字符串类型,使用字符数组存储字符串。

读写字符串
  • 读写单个字符:getchar()putchar(),效率比printf、scanf高。

  • 读写一个字符串:gets()puts()

getchar()

int getchar(void);

stdin 读取下一个字符(包括空格、换行符、制表符),等价于 getc (stdin)

  • 跳过一行多余的字符

    while( getchar() != '\n' ){
        continue;
    }
    

putchar()

int putchar( int ch );

stdout 写入一个字符,字符 ch 在写入前被转换为 unsigned char,等价于 putc(ch, stdout)

puts()

int puts( const char *str );

  • 将一个字符串写到 stdout,输出后自动添加 \n

  • 返回值:成功——非负值;失败—— -1。

gets()

char *gets( char *str );

  • 用于从stdin读取字符串(包含空格),直到出现 \n、EOF,在读取最后一个字符后立即添加 \0,并输出一个换行符。

    #include <stdio.h>
    int main(){
    	char c[20];	//字符数组的数组名存的是字符数组的起始地址,类型是字符指针(char[20]*)
    	gets(c);	//一次读取一行使用gets()
    	puts(c);	//等价于printf("%c\n",c);
    	return 0;
    }
    
  • scanf() 结合正则表达式使用,也可以实现**gets()**的效果。如:使用 %[^\n]s 获取带有空格的字符串。

    #include <stdio.h>
    int main() {
    	char str[10];
    	scanf("%[^\n]s", str);
    	printf("%s", str);
    	return 0;
    }
    

​ 由于 scanf()、gets() 都不知道要获取的字符串的大小,可能会发生越界(缓冲区溢出),因此 scanf()gets() 都是不安全的

字符串数组
  • 使用二维数组表示(有大量空字符)

    char week[][10] = { "Monday","Tuesday","Wednesday","Thursday",
    					"Friday","Saturday","Sunday" };
    
  • 使用字符指针数组表示(推荐)

字符串操作

搜索字符串末尾常用方法:

  • s 指向空字符

    while(*s){
        s++;
    }
    
  • s 指向空字符的下一个字符

    while(*s++)
        ;
    

​ 使用以上方法实现 strlen()strcat()

//my_strlen()
size_t my_strlen1(const char* s) {
	char* p = s;	//保存字符串首
	while (*p)		//p指向字符串末尾
		p++;
	return p - s;	
}
size_t my_strlen2(const char* s) {
	char* p = s;
	while (*p++)	//p指向字符串末尾的下一个字符
		;
	return p - s - 1;
}

//my_strcat()
char* my_strcat1(char* dest, const char* src) {
	char* p = dest;
	while (*p) 	//搜索字符串末尾
		p++;
	while (*p++ = *src++)
		;
	return dest;
}

指针

  • 指针:存放变量的地址,unsigned类型的整数

  • 指针变量 :存放地址(指针)的变量

  • & : 引用、取地址。不能取寄存器变量地址,因为寄存器变量不在内存中。

  • * : 解引用运算符,通过指针访问指针指向的对象。

声明

  • 基类型 *指针变量名

  • 同时声明多个指针变量时,* 只和第一个变量名结合。

    int    *ip; // 整型指针
    double *dp; // double 型指针
    float  *fp; // 浮点型指针
    char   *cp;	//字符型指针
    char   *p1, *p2, p3;//*p1 *p2是char型指针变量,p3是一个char型变量
    

​ 只做声明而不初始化的指针是一种野指针,不能直接使用。

初始化

  • 变量取地址
  • 另一个指针赋值
int variable, a[6];

//变量取地址初始化指针
int *p1 = &variable;
int *p2 = a;

int *q;
q = p2;//另一个指针初始化指针

间接访问

访问一个变量可以直接访问、间接访问,只有需要间接访问时才使用指针。

  • 间接访问:通过指向变量内存空间的指针访问变量(访问内存两次)
  • 直接访问:通过变量名访问(访问内存一次)
#include <stdio.h>

int main(){
    int a = 88;
    int *p = &a;	//定义指针并初始化,*p相当于a的别名
    printf("直接访问: a = %d\n", a);  //直接访问
    printf("间接访问:*p = %d\n", *p); //间接访问

    return 0;
}
  • *p 将p变量的内容取出,当做地址看待,并找到地址对应的内存单元。
    • ***p**做左值:存数据到对应的内存中
    • ***p**做右值:取出内存中的内容
野指针

未初始化、指向未知区域的指针是野指针。野指针并不会引发错误,但是操作野指针指向的内存区域会出现未定义的行为(不确定会发生什么)。

  • 指针未初始化、没有有效地址空间的指针。

    #include <stdio.h>
    
    int main() {
    	int* ip1;
    	*ip1 = 1000;	//使用了未初始化的局部变量ip1
    	printf("%p", ip1);
    
    	return 0;
    }
    
  • 指针变量对应的内存不可访问——访问权限冲突,地址0~255留给操作系统使用

    #incldue <stdio.h>
    
    //编译成功,但是引发了异常: 写入访问权限冲突
    int main() {
    	int* ip2 = 10;	//地址0~255留给操作系统使用,不要直接使用一个数值给指针变量赋值
    	*ip2 = 1000;
    	printf("%p", ip2);
    
    	return 0;
    }
    
空指针

​ 变量声明时,如果没有确切的地址可以赋值,可以为指针变量赋 NULL,eg:int *p = NULL; ,赋值为**NULL** 的***p就是空指针**。NULL 在C语言的标准库中的定义为**#define NULL ((void *)0)**,所以赋值为 NULL 的指针一定会发生访问权限冲突(因为0~255的地址由操作系统保留)。

泛型指针

void *p 可以接收任意一种变量地址,但是不能间接引用自身,必须强制类型转换为具体的数据类型

#include <stdio.h>
int main() {
	int a = 66;
	void* p = &a;	//void *p 万能指针、泛型指针
	//printf("%d", *p);		//不允许使用不完整的类型
	printf("%d", *(int *)p);//66  * 与()都是单目运算符,优先级相同,从右向左结合

	return 0;
}
指针大小

指针的大小取决于编译器的位数,与指针类型无关。因为一个地址是32、64位的unsigned型整数,所以一个指针占4、8字节。

  • 32位编译器,所有的指针都是32位(4字节)。

  • 64位编译器,所有的指针都是64位(8字节)。

    #include <stdio.h>
    int main() {
    	int* p1;
    	int** p2;
    	char* p3;
    	char** p4;
    
    	printf("sizeof(p1) = %d\n", sizeof(p1));
    	printf("sizeof(p2) = %d\n", sizeof(p2));
    	printf("sizeof(p3) = %d\n", sizeof(p3));
    	printf("sizeof(p3) = %d\n", sizeof(p4));
    	printf("sizeof(void *) = %d\n", sizeof(void *));
    
    	return 0;
    }
    
指针 & 数组
  • 数组名可以作为指针使用,它是指向索引值为0的元素的指针常量,在程序运行期间不能改变,不能被赋值

  • 指针可以作为数组名使用,即可以对指针使用[]运算符。由于指针是变量,可以用数组名给指针赋值

⇒ \Rightarrow 访问数组元素有两种方法:指针法 :*( );下标法 :[ ],且两者是等价的。

#include <stdio.h>

int main() {
	int arr[] = { 0,1,2,3 };
	int* p = arr;

	printf("arr[2] = %d\n", *(arr + 2));//数组名作为指针使用
	//arr++;		//数组名是指针常量,程序运行期间不能被改变
	printf("arr[2] = %d\n", p[2]);		//指针作为数组名使用

	return 0;
}
* & ++ - -

++--* 优先级相同,从右向左结合。--* 的组合同 ++* 的组合:

  • *p++ $\Leftrightarrow $ *(p++),表达式的值为*p,副作用是 p 自增
  • (*p)++ 表达式的值为 *p ,副作用是 *p 自增
  • *++p ⇔ \Leftrightarrow *(++p) 表达式的值为 *(p+1) ,副作用是 p 自增
  • ++*p ⇔ \Leftrightarrow ++(*p) 表达式的值为 *p+1,副作用是 *p 自增
#include <stdio.h>

int main() {
	int a1[] = { 2,4,6,8 };
	int* p = a1;
	int b1 = *p++;	//b1 = *(p++) = 2
	printf("a1[0]=%d, b1=%d, *p=%d\n", a1[0], b1, *p);	//2 2 4

	int a2[] = { 2,4,6,8 };
	int* q = a2;	//*q = 2
	int b2 = (*q)++;//<=> b2 = *q; b2++; =>	b2 = 2, a2[0] = 3
	printf("a2[0]=%d, b2=%d, *q=%d\n", a2[0], b2, *q);	//3 2 3

	int a3[] = { 2,4,6,8 };
	int* r = a3;	//*r = 2
	int b3 = *++r;	//b3 = *(++r) = 4
	printf("a3[0]=%d, b3=%d, *r=%d\n", a3[0], b3, *r);	//2 4 4

	int a4[] = { 2,4,6,8 };
	int* s = a4;	//*s = 2
	int b4 = ++*s;	//b4 = ++(*s) = 3
	printf("a4[0]=%d, b4=%d, *s=%d\n", a4[0], b4, *s);	//3 3 3

	return 0;
}
指针 & 一维数组
  • a[i] ⇔ \Leftrightarrow *(a+i),即 arr[i] == *(arr+i) == p[i] == *(p+i)
  • &array+1:偏移过整个数组 array。
#include <stdio.h>

int main() {
    int a[] = { 1,2,3,4,5,6 };
    int* p = a;	// <=> int *p = &a[0];
    
    //访问数组元素的方法
    printf("a[0]=%d\n", a[0]);		//1
    printf("*(a+0)=%d\n", *(a+0));	//1
    printf("p[0]=%d\n", p[0]);		//1
    printf("*(p+0)=%d\n", *(p + 0));//1

	//&数组名+1 => 偏移过整个数组
	printf(" a  = %p\n", a);		//a  = 00CFF790
	printf("&a+1= %p\n", &a + 1);	//&a+1= 00CFF7A8
	printf("sizeof(a)=%d\n", sizeof(a)); //sizeof(a) = 24 = 00CFF7A8-00CFF790
    
    return 0;
}
指针 & 二维数组
  • 对于二维数组有:a[i][j] ⇔ \Leftrightarrow *(*(a+i)+j),即 arr[i][j] == *(*(arr+i)+j) == p[i][j] == *(*(p+i)+j)

  • 行指针:int *row = arr[i]

  • 列指针:int *col = &arr[0][j]

    int a[3][4] = {{0,1,2,3},
                   {4,5,6,7},
                   {7,8,9,10}};
    // 列指针遍历第2列所有元素
    int* col = &a[0][1]; 
    for (int i = 0; i < 3; i++) {	
        printf("%d ", *( col + i * 4 ));
    }
    
  • 二维数组本质是二级指针

    #include <stdio.h>
    
    int main() {
    	int a[] = { 1,2,3 };
    	int b[] = { 4,5,6 };
    	int c[] = { 7,8,9 };
    	int* p1 = &a, * p2 = &b, * p3 = &c;
    	int* pointer_array[] = { p1,p2,p3 };//整型指针数组
    
    	//pointer_array[0][0] = *(pointer_array[0]) = **pointer_array = 1
    	printf("pointer_array[0][0] = %d\n", pointer_array[0][0]);
    	printf("**pointer_array = %d\n", **pointer_array);
    
        return 0;
    }
    
字符指针 & 字符数组

字符指针初始化时可以赋值字符串常量;字符数组初始化时也可以赋值字符串常量,但是实现方式有区别:

  • 使用 "" 初始化的字符型常量都是**const char** 型,存放在数据区,只能读不能写
  • char *p = "String constant";:将指针 p 指向数据区的字符串常量"String ocnstant"的首地址,不能修改。
  • char s[] = "String constant";:相当于 strcpy(s,"String constant"); 操作将常量从数据区拷贝到另一片地址。
#include <stdio.h>
#include <string.h>

int main() {
	char c[10] = "hello"; //等价于strcpy(c,"hello"); c为const char
	char* p = "hello";    //把字符串型常量"hello"的首地址赋给p

	c[0] = 'H';
	//p[0]='H'; //引发了异常: 写入访问权限冲突。不可以对常量区数据进行修改

	printf("c[0]=%c\n", c[0]);
	printf("p[0]=%c\n", p[0]);

	p = "world";  //将字符串world的地址赋给p,只是改变指针的指向
    //c = "world"; //表达式必须是可修改的左值 => 无法对const char型的数组c赋值
	puts(p);	  //world

	return 0;
}

​ 若取消 p[0]='H'; 的注释,运行时会有异常——写入访问权限冲突;若取消 c="world"; 的注释,会直接报错,无法编译。因为使用**""**初始化的字符串常量是 const char 型,存放在数据区,这片区域只能读不能写,所以会有以上现象。

字符指针数组
char* weekdays[] = { "Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday" };

[]* 优先级高,先于 [] 结合成为数组,再与 * 结合成为字符指针数组,即数组元素类型是 char *

在这里插入图片描述

指针运算

​ 若指针变量 p 指向某一数组元素a[x],即 p = &a[x],那么有:

  • p++p+=1*(p+1) 都会使 p 访问到下一个元素;p--p-=1 *(p-1)使 p 访问上一个数组元素。

    • p++p+=1 的副作用是改变了 p 的值,导致访问数组越界,同时可能导致指针 p 成为野指针

    • p+1不会改变 p 的值,但是 *(p+i) 仍可能会发生访问数组越界,但不会导致指针 p 成为野指针。

#include <stdio.h>

int main() {
	int a[6] = {1,2,3,4,5,6};
	int* p, * q, * r;
	p = q = r = &a[4];
	
	printf("a[4] = %d, &a[4] = %p\n", a[4], &a[4]);//a[4] = 5, &a[4] = 001AFB78

	//p++ 会改变 p 的值,导致访问数组越界,p成为野指针
	p++;	printf("p = %p,*p = %d\n", p, *p);	//p = 001AFB7C,*p = 6
	p++;	printf("p = %p,*p = %d\n", p, *p);	//p = 001AFB80,*p = -858993460

	//q+=1 会改变q 的值,导致访问数组越界,q成为野指针
	q += 1;	printf("q = %p,*q = %d\n", q, *q);	//q = 001AFB7C,*q = 6
	q += 1;	printf("q = %p,*q = %d\n", q, *q);	//q = 001AFB80,*q = -858993460

	//r+i 不会改变r的值,但是也可能发生访问数组越界,但不会成为野指针
	r + 1;	printf("r = %p,*r = %d\n", r, *(r+1));//r = 001AFB78,*r = 6
	r + 1;	printf("r = %p,*r = %d\n", r, *(r+2));//r = 001AFB78, * r = -858993460

	return 0;
}

由以上例子可知:p++p+=1*(p+1)都使得指针 p 访问到了后面的一个元素,都有可能发生访问数组越界,程序执行时 p+1 不会改变指针指向,但是 p++p+=1改变了指针的指向,使得指针指向了数组外的未知区域,从而成为了野指针。

指针运算
  • p + 整数

    • p+ia+i 从当前位置 a[x] 向后偏移 i 个元素;
    • p-ia-i 从当前位置 a[x] 向前偏移 i个元素。
  • p2 - p1 (指针的比较)

    • p1、p2当指向同一数组时,表示两个指针所指元素之间的相对距离(间隔几个元素)
    • p1、p2指向两个普通变量时无意义
  • 指针不能进行算术***、/、%**,无法通过编译。

    int main() {
    	int a[10] = { 1,2,3,4,5,6,7,8 };
    	int* p = &a[5];
    	printf("%p p:%d\n", p, *p);	//6
    
    	//p++ p=p+1 指针指向下一个元素
    	p++;	printf("%p p++:%d\n", p, *p);//7
    	p += 1;	printf("%p p+1:%d\n", p, *p);//8
    
    	//p-- p=p-1 指针指向上一个元素
    	p--;	printf("%p p--:%d\n", p, *p);//7
    	p -= 1;	printf("%p p-1:%d\n", p, *p);//6
    
    	//从当前元素向前偏移(p-i)、向后偏移(p+i)
    	p -= 2;	printf("%p p-=2:%d\n", p, *p);//4
    	p += 4;	printf("%p p+=4:%d\n", p, *p);//8
    
    	//p1-p2 表示指针所指元素之间间隔的元素个数
    	int* p1 = &a[2];
    	int* p2 = &a[6];
    	printf("p2 - p1 = %d\n", p2 - p1);	//4
    
    	return 0;
    }
    
* & const

const 修饰指针时,不考虑指针的类型,将 const *向右结合,被修饰的部分(p、p)变为只读。常用在函数形参内,限制指针所对应空间为只读,增加程序健壮性。

  • const修饰 *p:指针指向内存中的值不可变,eg:const int *pint const *p能修改p,不能修改 *p

    int a = 1, b = 6;
    const int* p = &a;
    //*p = 8;	//取消本行注释编译器报错:表达式必须是可修改的左值
    p = &b;
    
  • const修饰 p:指针指向的位置不可变,eg:int * const p能修改 *p,不能修改p。

  • const同时修饰 *p、p:指针指向的位置、位置中的值都不改变,eg:const int *const p 中的 *p、p 两者都不能修改。

#include <stdio.h>
int main() {
	int a[6] = {1,2,3,4,5,6};

	const int* p = a;	//*p不可变,p可变
	//(*p)++;			//表达式必须是可修改的左值
	p++; printf("a = %p, &p = %p\n", a, p);//a = 0137FA54, &p = 0137FA58

	int const* q = a;	//*q不可变,q可变 <=>const int* q = a;
	//*q = 2;			//表达式必须是可修改的左值
	q++; printf("a = %p, &q = %p\n", a, q);//a = 0137FA54, &q = 0137FA58

	int* const r = a;	//*r可变,r不可变
	//r++;				//表达式必须是可修改的左值
	printf("a[0] = %d\n", a[0]);	//a[0] = 1
	*r = 2;	
	printf("a[0] = %d,*r = %d\n", a[0], *r);//a[0] = 2,*r = 2

	const int* const s = a;	//*r r都不可变
	//*s = 4;				//表达式必须是可修改的左值
	//s+=1;					//表达式必须是可修改的左值

	return 0;
}
指针数组
  • 存储元素全部是指针(地址)的数组

  • 指针数组本质是二级指针

    #include <stdio.h>
    
    int main() {
    	int a = 1, b = 2, c = 3;
    
    	int *p1 = &a;
    	int *p2 = &b;
    	int *p3 = &c;
    
    	int *pointer_array[] = { p1,p2,p3 };//整型指针数组
    
        //指针数组第一个元素 *pointer_array[0] = 1,二级指针 **pointer_array = 1
    	printf("*pointer_array[0] = %d\n", *pointer_array[0]);
    	printf("**pointer_array = %d\n", **pointer_array); //pointer_array[0] = *(pointer_array+0)
    
    	return 0;
    }
    
二级指针

赋值:一级指针取地址

  • 要想在函数中改变变量的值,必须把变量的地址传进去
  • 要想改变函数中指针变量的值,必须把指针变量的地址传进去
#include <stdio.h>

void change(int **pi,int *pj){
	*pi = pj;	//把一级指针的地址给二级指针,修改二级指针的指向
}

int main(){
	int i = 4, j = 6;
	int *pi = &i, *pj = &j;
	printf("i=%d,*pi=%d; j=%d,*pj=%d\n", i, *pi, j, *pj);
    
	change(&pi, pj);	//pi是一级指针,对一级指针取地址是二级指针
	printf("After changing:i=%d,*pi=%d,j=%d,*pj=%d\n", i, *pi, j, *pj);
    
	return 0;
}

运行结果:

内存管理
堆、栈区别
  • 栈空间在函数调用时创建,调用结束就销毁。不能使用一个指针变量访问被调函数的栈空间内的变量。
  • 堆空间由**malloc**申请,若不用 free 释放申请的空间,则在进程执行过程中一直可以访问。堆空间不会随子函数的结束而释放,必须自己释放。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* print_stack(){
	char stack[] = "I'm a stack!";
	puts(stack);	//能正常打印
	return stack;
}

char* print_heap(){
	char write_to_heap[] = "I'm a heap!";
	char* heap = (char*)malloc(sizeof(write_to_heap));
	strcpy(heap, write_to_heap);	//往刚分配的堆空间写数据
	puts(heap);
	return heap;
}

int main(){
	char* stack, * heap;

	stack = print_stack();	//函数调用创建了函数栈,栈空间会随着函数的执行结束而释放
	puts(stack);	 		//打印不出来,指针变量不能指向被调函数的栈空间内的变量

	heap = print_heap();
	puts(heap);

	return 0;
}

运行结果:
在这里插入图片描述

内存泄漏
  • 程序运行时动态分配的堆内存由于某种原因未释放、不能释放,成为系统垃圾,拖慢程序运行速度或使系统奔溃。
  • 及时释放不用的内存块避免内存泄露
int *p = (int *)malloc(sizeof(int)); //1号内存
int *q = (int *)malloc(sizeof(int)); //2号内存
p = q;   //p指向了q,导致没有指针指向1号内存,未及时释放而成为了垃圾

函数

  • 作用:提高程序复用性、可读性,使程序更加模块化,分为库函数用户自定义函数
  • 声明:告诉编译器函数的名称、返回值类型、形式参数的个数、类型、顺序。隐式声明默认返回值类型是整型。
  • 定义:函数原型(函数名、返回值类型、形参)+ 函数体,指明函数要做什么。
  • 调用:表明在此处执行函数。
  • 栈帧:发生函数调用时,系统会在栈区开辟一片空间,用来存放函数的形参、局部变量,当函数调用结束时销毁。

​ 声明函数时并不会为函数原型中的变量分配内存,当发生函数调用时才会给函数定义中的形参分配空间、初始化为实参表达式的值。

参数传递

实参:函数调用的括号中的内容,可以是常量、变量、表达式,但必须是确定的值。

形参:函数的自变量,用来接收调用该函数时传入的参数。本质是一个名字,不占用内存空间,发生函数调用时才分配内存单元,调用结束后释放。

实参与形参应该个数相等、类型匹配、顺序对应、一 一传递数据。实参 -> 形参的数据传递特点如下:

  • 单向一次性的值传递(只能由实参 -> 形参传值,类型不能传递)

  • 将实参的值复制给形参实现值传递,在函数内部不能修改实参的值。(传值不能改变实参的值)

    #include <stdio.h>
    void swap(int arg_a, int arg_b) { //1 2
    	int temp = arg_a;	//temp=1
    	arg_a = arg_b;		//arg_a=2
    	arg_b = temp;		//arg_b=1
    }						//形参arg_a arg_b交换了值,但实参a,b值不变
    int main() {
    	int a = 1, b = 2;
    	swap(a, b);	//将a=1,b=2复制给arg_a、arg_b,
    	printf("a = %d, b = %d\n", a, b);	//a = 1, b = 2
    	return 0;
    }
    
  • 实参是一个地址,则形参是指向实参所指对象的指针,在函数内部可以改变实参的值。(传址也是传值的一种,地址是一个十六进制的无符号数)

    #include <stdio.h>
    void swap(int* a, int* b) {
    	int temp = *a;
    	*a = *b;
    	*b = temp;
    }
    int main() {
    	int a = 1, b = 2;
    	int* p1 = &a;
    	int* p2 = &b;
    	swap(p1, p2);
    	printf("a = %d, b= %d\n", *p1, *p2);//a=2 b=1
    	return 0;
    }
    
  • 形参相当于局部变量,不能在函数内部再定义与形参同名的局部变量,否则会编译不通。

一维数组作形参
  • 数组名作为实参传递给函数时,弱化为指针。传递的是数组,但是在函数中是使用指针遍历数组元素

  • 数组array做函数形参时,*char array[ ] ⇔ \Leftrightarrow char array[10] ⇔ \Leftrightarrow char array

  • 一维数组作形参传递时函数接收到的是数组名(一个地址),所以会丢失类型信息、数组长度,故一维数组作形参时,需要传递数组的长度

    #include <stdio.h>
    
    void print1(int b[]) {
    	int i = 0;
    	for (i = 0; i < sizeof(b) / sizeof(b[0]); i++) { //!!!!!!典型错误
    		printf("a[%d]:%d  ", i, b[i]);
    	}
    	putchar('\n');
    }
    void print2(int b[], int len) {
    	int i = 0;
    	for (i = 0; i < len; i++) { 
    		printf("a[%d]:%d  ", i, b[i]);
    	}
    	putchar('\n');
    }
    
    int main() {
    	int a[4] = { 0,1,2,3 };
    	print1(a);	//x64输出 a[0]:0  a[1]:1	x64:sizeof(b) / sizeof(b[0]) = 2
    				//x86输出 a[0]:0		x86:sizeof(b) / sizeof(b[0]) = 1
    	print2(a, 4);//a[0]:0  a[1]:1  a[2]:2  a[3]:3 
    	return 0;
    }
    
  • 好处:避免了复制所有数组元素;可以修改数组元素的值;操作更加灵活。

    #include <stdio.h>
    
    void change1(char array[]){	//传递给形参array变为char *
        array[0] = 'H';
    }
    void change2(char array[10]){
        array[0] = 'h';
    }
    void change3(char* array){
        //多种赋值方法,操作更加灵活
        *array = 'H';
        *(array + 2) = 'L';
        array[3] = 'L';
    }
    
    int main(){
        char c[10] = "hello";
        change1(c);	puts(c);//Hello 实参c是char型
        change2(c);	puts(c);//hello
        change3(c);	puts(c);//HeLLo
        return 0;
    }
    
二维数组作形参
  • 二维数组作形参传递时,可以忽略行的信息,不能忽略列的信息。

    i_j_address = base_address + i * column * sizeof(element_type) + j * sizeof(element_type)

    #include <stdio.h>
    
    int sum(int arr[][4], int n) {	//n为行
        int sum = 0;
        for (int i = 0; i < n; i++){
            for (int j = 0; j < 4; j++){
                sum += arr[i][j];
            }
        }
        return sum;
    }
    
    int main() {
        int matrix[4][4] = { {1,2,3,4},{2,2,3,4} };
        printf("sum(matrix)=%d\n", sum(matrix, 4));
        return 0;
    }
    
  • 传递列数不固定的二维数组——指针数组

指针作函数形参

在函数传参过程中,值传递不能改变实参的值,若传递实参的地址,可以用指针改变实参的值。

//指针作形参,求数组最大值最小值
#include <stdio.h>

void find_min_max(int* p, int len, int *min, int *max) {
	*min = p[0];
	*max = p[0];
	for ( int i = 0; i < len; ++i ) {
		if ( p[i] > *max )
			*max = p[i];
		else if ( p[i] < *min )
			*min = p[i];
	}
}

int main() {
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int min, max;
	find_min_max(arr, 10, &min, &max);
	printf("min=%d,max=%d", min, max); //min=0,max=9

	return 0;
}
return & exit()
  • return:带着函数的返回值返回到函数调用处,返回值不是数组。
  • exit( ):直接退出当前程序
#include <stdio.h>

int test1(int arg) {
	printf("test1()…arg=%d\n", arg);
	return 0;
}

int test2(int arg) {
	printf("test2()…arg=%d\n", arg);
	exit(0);	//执行后直接退出程序,不再返回程序调用处
}

int main() {
    /*屏幕上输出:
    test1()…arg=6
	test1()返回值:0
	*/
	printf("test1()返回值:%d\n", test1(6));
	printf("test2()返回值:%d\n", test2(8));//test2中的exit(0)执行后直接退出程序,所以该语句以及以下语句都不再执行
	return 0;
}
作用域
全局变量
  • 定义在函数外面的变量,存储在数据段,在整个进程的执行过程中始终有效。尽量不用!!!
  • 全局变量的作用域是从定义开始到文件末尾
#include <stdio.h>

int g = 6;	//全局变量g程序运行期间一直存在
void func1(int a) {
	printf("func1 g=%d\n", g);	//无论实参传多少始终输出g = 6
}
int main() {
	printf(" main g=%d\n", g);	//6
	func1(1);
    return 0;
}
局部变量
  • 定义在函数内部,存储在函数的栈空间,函数调用时分配空间,执行结束时释放空间

  • 局部变量的作用域是块,即最近的一对 {} ,在 {}外不能访问。

    #include <stdio.h>
    
    int main() {
    	{
    		int local = 1;	//在`{}`外不能访问变量local
    	}
    	//printf("local=%d", local); //取消注释会报错:local未定义
    
    	return 0;
    }
    
  • 就近原则:若局部变量与全局变量重名,获取、修改的值为局部变量

    #include <stdio.h>
    
    int g = 6;	//全局变量g
    
    void func1(int a) {
    	printf("func1 g=%d\n", g);	//全局变量g
    }
    
    int main() {
    	int g = 8;					//局部变量g
    	printf(" main g=%d\n", g);	// main g=8	 局部变量g和全局变量g 同名 => 局部变量g(就近原则)
    	func1(1);					//func1 g=6
    
        return 0}
    
  • 形参也相当于局部变量,与全局变量同名时同样遵循就近原则

    #include <stdio.h>
    
    int g = 6;			//全局变量g
    void func2(int g) {	//局部变量g (形参相当于局部变量,所以此处的g是func2函数栈空间内的同名局部变量g=1
    	printf("func2 g=%d\n", g);	//func2 g=1(局部变量与全局变量同名,所以此处的g使用的是函数栈空间内的局部变量g的值)
    }
    
    int main(){
        printf(" main g=%d\n", g);	// 数据段全局变量g => main g=6
       	func2(1);	//实参=1 => 形参g=1
    
        return 0;
    }
    
递归

函数自身调用自身的操作,必须要有递归结束的条件。

#include <stdio.h>

int factorial(int n) {
	return n>=2?( n * factorial(n - 1)):1;	//递归结束条件:n==1
}

int main() {
	printf("4!= %d", factorial(4));
	return 0;
}

何时用递归:一个大问题可以分解为多个小问题,且小问题的求解方式和大问题一样,就可以将上述小问题的解合并为大问题的解。

注意:边界条件;递推公式

斐波那契数列

{ F ( 0 ) = 0 , n = 0 F ( 1 ) = 1 , n = 1 F ( n ) = F ( n − 1 ) + F ( n − 2 ) , n ⩾ 2 \left \{ \begin {matrix} F(0) & = & 0 & , & n=0 \\ F(1) & = & 1 & , & n=1 \\ F(n) & = & F(n-1) & + & F(n-2), & n \geqslant 2\\ \end {matrix} \right. F(0)F(1)F(n)===01F(n1),,+n=0n=1F(n2),n2

  • 递归方法会重复计算大量值;非递归则会保存重复计算的值。

    #include <stdio.h>
    
    //递归 太多值被重复计算
    long long fib1(int n){
    	return (n>=0&&n<=1)?n:fib1(n - 1) + fib1(n - 2);
    }
    //非递归 保存需要重复计算的值
    long long fib2(int n) {
    	int a = 0, b = 1;	//F(n-2) F(n-1)
    	if ( n == 0 ) return a;		//F(0)
    	else if ( n == 1 ) return b;//F(1)
    	else 
    		for ( int i = 2; i <= n; i++ ) {
    			int tmp_i = b + a; //第i项 F(n) = F(n-1)+F(n-2)
    
    			//准备第i+1项的F(n-1) F(n-2)
    			a = b;
    			b = tmp_i;	
    		}
    	return b;	//F(n)
    }
    
    int main() {
    	printf("%lld\n", fib1(8));
    	printf("%lld\n", fib2(16));
    	return 0;
    }
    
  • 通项公式: a n = ( 2 / ( 5 + 1 ) − 1 / ( 5 + 1 / 2 ) n ) / 5 a_{n}=(2/ (\sqrt{5}+1)-1/ (\sqrt{5}+1/2)^{n}) / \sqrt{5} an=(2/(5 +1)1/(5 +1/2)n)/5 ,由于浮点数是非精确值,不能使用通项公式求斐波那契数列

  • 线性代数:由推导过程可知只需求得
    ( 1 1 1 0 ) n \begin {pmatrix} 1 & 1\\ 1 & 0\\ \end{pmatrix} ^{n} (1110)n 的值就可以知道斐波那契数列的某个值,时间复杂度为O(log n)。
    { F n + 1 = F n + F n − 1 F n = F n \left \{ \begin {matrix} F_ {n+1}& =& F_{n} &+&F_{n-1}\\ F {n} & = &F {n} & \\ \end{matrix} \right. {Fn+1Fn==FnFn+Fn1

    ⇒ ( F n + 1 F n ) = ( 1 1 1 0 ) ⋅ ( F n F n − 1 ) = ( 1 1 1 0 ) n ⋅ ( F 1 F 0 ) = ( 1 1 1 0 ) n ⋅ ( 1 0 ) \Rightarrow \begin {pmatrix} F_ {n+1}\\ F_ {n}\\ \end{pmatrix} = \begin {pmatrix} 1 & 1\\ 1 & 0\\ \end{pmatrix} \cdot \begin {pmatrix} F _ {n}\\ F {n-1} \\ \end{pmatrix} =\begin {pmatrix} 1 & 1\\ 1 & 0\\ \end{pmatrix} ^{n} \cdot \begin {pmatrix} F_{1} \\ F_{0} \\ \end {pmatrix} =\begin {pmatrix} 1 & 1\\ 1 & 0\\ \end{pmatrix} ^{n} \cdot \begin {pmatrix} 1 \\ 0 \\ \end {pmatrix} (Fn+1Fn)=(1110)(FnFn1)=(1110)n(F1F0)=(1110)n(10)

内联函数

​ 在实现函数体时使用**inline()**修饰的函数就是内联函数,在调用内联函数时,内联函数体内容直接替换函数调用

  • 内联函数只适合函数体内结构简单的函数使用(没有while、switch,不能是递归函数)
  • inline 修饰的函数体必须在头文件中。若 inline 修饰的函数定义在头文件中,实现在其他文件中,会发生链接错误。(因为inline在编译阶段就发生了替换,压根没有进行链接)

结构体

C语言的结构体像其他高级语言的类,eg:定义一个“学生”

typedef struct student {
	int num;
	char name[20];
	bool gender;//0-man 1-woman
	int age;
}Student;

初始化

Student s1 = { 001,"Shine",0,18 };
Student s2 = { 002,"Lisa",1 }; //未初始化的成员赋值为0
Student s3 = s1;

访问成员

  • . :结构体变量、结构体指针变量都可以使用。
  • -> :当使用到结构体指针时用,相当于(*p).
作函数参数
  • 值传递 —— 结构体作函数参数时会复制结构体的全部数据
  • 使用结构体指针作函数参数避免复制结构体的全部数据
#include <stdio.h>
#include <stdbool.h>

typedef struct student {
	int num;
	char name[20];
	bool gender;//0-man 1-women
	int age;
}Student;

void print1(Student s) {	//结构体作参数(值传递)拷贝整个结构体数据
	printf("%d %s %d %d\n", s.num, s.name, s.gender, s.age);
}
void print2(Student* s) {	//避免复制全部数据使用结构体指针
	printf("%d %s %d %d\n", (*s).num, (*s).name, s->gender, s->age);//s->num <=> (*s).num
}

int main() {
	//初始化
	Student s1 = { 001,"Shine",0,18 };
	Student s2 = { 002,"Lisa",1 }; //未初始化的成员赋值为0

	//赋值	.访问成员  
	Student s3;
	s3 = s2;
	s3.age = 16;
	printf("s3:%d %s %d %d\n", s3.num, s3.name, s3.gender, s3.age);//s3:2 Lisa 1 16

	print1(s2);	//2 Lisa 1 0
	print2(&s3);//2 Lisa 1 16

	return 0;
}

何时分配内存

  • 定义结构体类型之后,未定义结构体变量之前 只是增加了一种数据类型,不会分为它配内存

  • 定义结构体指针之后申请了一个指针,并告诉系统这个指针是干什么用的,至于指针指向的地址并不确定。由于指针本身要占用内存,只分配了指针所需大小的内存,这个分配是使用 malloc() 手动分配的,系统并不会自动给它分配空间

  • 定义了结构体变量系统会自动为该变量分配内存空间。

    typedef struct Lnode{
    	Elemtype data;
    	struct Lnode *next;
    }* LinkList, Lnode;	//指向Lnode结构体的指针别名是LinkList,结构体别名是Lnode
    
    int main(){
    	LinkList L;		//声明了一个Linklist型结构体指针
        L = (Lnode *)malloc(sizeof(Lnode));	//手动使用malloc为指针分配空间
    	if (L==NULL)
    		return false;	//内存不足,分配结点失败
    	L->next = NULL;		//头结点之后没有其他节点,空的带头结点单链表形成
    	return 0;
    }
    

枚举

  • 枚举类型的 枚举元素常量处理,不能取成员变量
  • 每一个枚举元素都代表一个整数,从 0 开始,可以在定义中为枚举元素指定值
  • 枚举元素可以用来判断比较 eg: if(workday == monday)if(workday > sunday)
#include <stdio.h>

typedef enum day {
    monday = 1,
    tuesday,
    wednesday,
    thursday,
    friday,
    saturday = 7,
    sunday
} Week;

void print_week(Week * e){
    printf("%d %d %d %d %d %d %d\n", monday, tuesday, wednesday,
           thursday, friday, saturday, sunday); 
}

int main() {
    int a = 7;
    Week weekend;

    print_week(&weekend);   //1 2 3 4 5 7 8
    weekend = (enum day)a;  //类型转换
    //weekend = a;          //错误
    printf("weekend:%d", weekend);
    
    return 0;
}

共用体

  • 多个变量使用同一块地址空间,但是任何时候只能有一个成员有值。共用体变量中起作用的成员是最后一次被赋值的成员。
  • 共用体大小由最大的成员大小决定
  • 共用体+结构体:共用体可以出现在结构体定义中,结构体也可以出现在共用体定义中
  • 共用体+数组:数组可以作为共用体成员,可以使用共用体数组
#include <stdio.h>

union Data{
   int i;
   float f;
   char  str[20];
};

int main( ){
   union Data data;

   data.i = 10;
   printf( "data.i : %d\n", data.i);	//10

   data.f = 220.5;
   printf( "data.f : %f\n", data.f);	//220.500000

   strcpy( data.str, "C Programming");
   printf( "data.str : %s\n", data.str);//C Programming

   return 0;
}

位域

  • 把一个字节中的二进制位划分为几个不同的区域,并给划分的区域命名、指定区域位数,这样的数据结构称为“位域”。

  • 定义与结构体相仿,使用和结构成员使用相同

    struct 位域结构名 {
    	类型	位域域名1:宽度;
    	类型	位域域名2:宽度;
    };
    
    位域变量名.位域名
    位域变量名->位域名
    
  • 典型应用:使用1位二进制位存放开关量

    #include <stdio.h>
    
    typedef struct SwitchValue {
    	unsigned int switch_value1 : 1;	//给划分的区域命名、指定宽度为1bit
    	unsigned int switch_value2 : 1;
    }status;
    
    int main() {
    	printf("%d", sizeof(status));	//4
    
    	return 0;
    }
    
  • 注意事项:

    • 位域可以是无名位域,但此时只能用来填充、调整位置,并不能使用
    • 位域宽度不能超过他的成员变量使用的数据类型的宽度

C标准库

<stdio.h>
fgets()

char *fgets(char *s, int size, FILE *stream)

  • 获取一个字符串,提前预留一个 '\0' 的位置,保证获取到的一定是有效的字符串,不会再产生越界,弥补了gets的缺点。
  • 存储字符串的空间足够时读取 '\n' ,空间不足时舍弃 '\n'
#include <stdio.h>
int main() {
	char str[10];
	fgets(str, sizeof(str), stdin);

	printf("%s", str);
	return 0;
}

运行结果:

fputs()

int fputs(const char *str, FILE *stream)

  • 返回值:成功——非负值;失败—— -1。
  • 输出字符串后不添加 \n
<stdlib.h>
内存管理
malloc

void* malloc( size_t size );

  • 分配成功时,返回指向新内存的指针。
    为避免内存泄漏,在使用完该区域之后,必须使用 free() 释放它、realloc() 重新分配。
  • 分配失败时,返回空指针。
calloc

void* calloc( size_t num, size_t size );

  • 分配 numsize 大小字节的空间并初始化为0
    • 分配成功:返回该片空间的首地址
    • 分配失败:返回空指针
realloc

void *realloc( void *ptr, size_t new_size );

  • 重新调整已分配的内存块大小,调整成功返回首地址,否则返回空指针。
  • ptr 应该指向先前已经分配的内存块
free

void free( void* ptr );

释放 malloc()calloc()realloc() 分配的内存,有可能造成指针悬空

随机数

void srand( unsigned seed ); 设定伪随机数种子

int rand(); 生成伪随机数

//生成十个伪随机数
srand(time(NULL));
for (int i = 0; i < 10; i++)
    printf("%d\n", rand());
system

int system( const char *command );

执行一条系统命令、调用系统工具,如cmd、mspaint(画图工具)、notepad(记事本)。

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

int main() {
	//system("cmd");		//唤醒Windows记事本
	//system("mspaint");	//唤醒画图

	//打印时间
	int h, m, s;//时分秒
	printf("输入时分秒:\n");
	scanf("%d %d %d", &h, &m, &s);
	for (; h < 24; h++) {
		for (; m < 60; m++) {
			for (; s < 60; s++) {
				printf("%02d:%02d:%02d", h, m, s);
				Sleep(1000);	//windows.h
				system("cls");	//stdlib
			}
		}
	}
	return 0;
}
<string.h>
memset

void *memset( void *dest, int ch, size_t count );

  • 按字节赋值 。由于0的补码全是0,-1 的补码全1,所以可以用来给数组的所有元素赋0、-1。

    memset(a, 0, sizeof(a)); 	//赋初值0
    memset(a, -1, sizeof(a));	//赋初值-1
    
  • 用于初始化一片内存,初始化任何数组(字符数组、整型数组、浮点型数组、结构体数组)

    typedef struct Student{
        int num;
        int age;
    }student;
    
    student s[4];
    memset(s,0,sizeof(s));
    
strlen

size_t strlen( const char *str );

  • 返回字符串数组有效长度\0 即停止,包括\n,不包括\0

    #include <stdio.h>
    #include <string.h>
    int main(){
      char str1[] = "String\n hello";	// 13
      char str2[] = "String\0 hello";	// 6
      printf("str1: %d\nstr2: %d", strlen(str1), strlen(str2));	//13 6
      return 0;
    }
    
strcpy

char *strcpy( char *dest, const char *src );

把 src 指向的字符串复制给 dest ,不检查数组是否会越界,所以需要在确保数组不会越界时再使用 strcpy(),否则使用 strncpy()

  • const char *src 说明 src 指向的内容不会被修改,作传入参数。 指向的内容 有 const修饰时,可以使用字符串常量代替字符串数组。
  • char *dest 表明通常会在函数内部修改 dest 指向的内容,作传入传出参数。
strncpy

char *strncpy( char *dest, const char *src, size_t count );

从字符串src复制一定数量的字符到dest

  • count ≤ strlen(src) ,不会向 dest 中写入\0;
  • count > strlen(src) ,会向 dest 中写入 \0,直到够 count 个字符。
#include <stdio.h>
#include <string.h>

int main() {
	//strcpy不会进行越界检查,确保不会越界时再使用
	char s1[10], s2[10];
	strcpy(s1, "hello.");
	puts(s1);	//hello.
	
	strncpy(s2, "hello", 3);//s2:hel
	strncpy(s2, "hello", 5);//s2:hello
	strncpy(s2, "hello", 6);//s2:hello\0
	strncpy(s2, "hello", 8);//s2:hello\0\0\0

	return 0;
}
strcmp

int strcmp( const char *str1, const char *str2 );

按字典序比较 str1与 str2 的ASCII值,并返回一个值

  • 返回值为 -1:str1 < str2
  • 返回值为 0:str1 = str2
  • 返回值为 1:str1 > str2
strcat

char *strcat( char *dest, const char *src );

  • src 的内容添加到 dest 的末尾,不会做边界检查,确定不会发生越界、溢出时使用,否则使用strncat()
  • 始终都会在添加字符结束后写入 '\0' ,即便会造成溢出也会写入(例如取消下面代码中的 strncat(s1, "cat", 3);
strncat

char *strncat( char *dest, const char *src, size_t count );

  • src 的内容中的至多 count 个字符添加到 dest 的末尾,
  • 始终会向末尾添加 '\0'
  • 惯用法: sizeof(dest) - strlen(dest) - 1 (提前预留 '\0' 的位置)
#include <stdio.h>
#include <string.h>
int main() {
	char s[12] = "cat";

	strcat(s, " +str");
	puts(s);	//cat +str

	char s1[10]="hello";
	strncat(s1, "cat", 2);//helloca\0\0\0
	//hellocacat\0 取消下行注释会发生异常,因为s1能放10个字符,但是向s[9]写入't'后仍会继续写入一个'\0'
	//strncat(s1, "cat", 3);	  

	//strncat惯用法: sizeof(s) - strlen(s) - 1 (-1是为了预留'\0'的位置)
	char a[] = "++++";
	strncat(s, a, sizeof(s) - strlen(s) - 1);
	puts(s);

	return 0;
}

其他

编译过程
  1. 预处理gcc -E *.c -o *.i
    • 取消注释、添加行号、保留 #pragma 编译器指令
    • 处理条件编译指令 #if#ifdef
    • 宏替换,替换宏定义、(含参)宏函数(文本替换)
    • 展开 #include <xxx.h> 包含的头文件(复制到对应位置),可以展开任意文件,且不检查语法错误*.c 文件变成 *.i 文件。
  2. 编译 : gcc -S *.c、*.i -o *.s
    • 单个文件逐行进行词法、语法、语义分析,最为耗时
    • 生成汇编代码文件
  3. 汇编 : gcc -c *.c、*i、*s -o *.o
    • 汇编代码转换位二进制文件,*.s 文件变为 *.obj 目标文件。
  4. 链接 : gcc *c、*i、*s -o *.exe
    • 引入库文件
    • 通过链接把多个目标文件(*.obj)关联到一起,解决外部内存地址问题 (一个文件的变量使用其他文件的变量)

​ 每个项目都会被编译为一个可执行文件,方便解决错误。

命令行参数

程序开始运行时,操作系统会调用 main() 函数,它分为:

  • 不含参数的 main(void)

  • 含参 main(int argc, char *argc[])

    • argc:执行命令时传递的参数个数
    • *argv[]:要传递的参数,第一个参数是 *argv[0],表示生成的可执行程序的路径,*argv[i] 表示要传递的第 i+1 个参数
    • 传参方式:可执行程序路径 参数2 参数3
    #include <stdio.h>
    
    int main(int argc, char *argv[]){
        printf("argc = %d\n", argc);
        for (int i = 0; i < argc; i++){
            puts(argv[i]);
        }
    
        return 0;
    }
    /*执行:./a.out hello
      输出:./a.out
      		hello
      */
    
宏函数

#define定义的宏函数效率比普通函数高,这是因为普通函数使用时有诸多开销:函数调用、保存寄存器值、传参、保存下一条指令地址、返回调用处、传递返回值、恢复寄存器值。

  • 左括号紧贴宏函数名,宏函数的整个表达式加 ()
  • 含参宏函数的每个参数都应该加 ()
  • 多语句宏函数使用do{} while(0)实现只执行一次的效果
  • 注意宏函数中多次++--副作用
#include <stdio.h>

#define FUNC(x) x + x * x
#define FUNC1(x) (x + x * x)
#define FUNC2(x) (x) + (x) * (x)
#define FUNC3(x) ((x) + (x) * (x))

#define HELLO() printf("Hello ");\
				printf("world\n")
#define HELLO1() do{ printf("Hello ");printf("world\n");}while(0)

int main(void) {
	int a = 4;
	int b = 6;

	printf("a=%d b=%d\n", a, b);

	//整个宏函数表达式外应该有(),否则可能会出错
	printf("FUNC(a) = %d\n", 2 * FUNC(a));	// => 2 * a + a * a = 24

	//含参宏函数的每个参数应该加(),否则可能出错
	printf("FUNC1(a+b) = %d\n", FUNC1(a+b));	// => a+b + a+b * a+b = 44
	printf("FUNC2(a+b) = %d\n", FUNC2(a+b));	// =>(a+b) + (a+b) * (a+b) = 110

	//宏函数多次副作用,不要使用++、--
	printf("FUNC3(++b) = %d\n", FUNC3(++b));	//156 ((++b) + (++b) * (++b))

	//多语句宏函数
	putchar('\n');
	if (0)
		HELLO();	//=> if (0) printf("Hello "); printf("world\n"); =>world

	//2.do{ }while(0)
	HELLO1();	//do{ printf("Hello ");printf("world\n");}while(0);

	return 0;
}
const
  • const修饰的常量不能用来指定数组长度

  • const定义的常量可以使用指针修改

    #include <stdio.h>
    int main() {
    	const int a = 66;
        //int arr[a];	//表达式必须含有常量值
    	int* p = &a;
    	*p = 11;
    	printf("a=%d", a); //11
    	return 0;
    }
    
  • const & 数组

  • const & 指针

运算符优先级

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值