C语言基础

1
2
scoop install mingw #windows
sudo apt install gcc #ubuntu/wsl
1
gcc -v

https://pan.lmio.xyz/pic/98257bac12069f45b8e2d80d5b41a6fa.png


关键字 名称 大小(64位系统) 值域
int 整型 4字节 -2^31, 2^31-1
float 浮点型 4字节
long int 长整型 8字节 -2^63, 2^63-1
long long int 长长整型 8字节
short 短整型 2字节 -2^15, 2^15-1
bool 布尔类型 1字节 0, 1
unsiged int 无符号整型 4字节 0, 2^32-1
  • 1字节=8比特

  1. 整型数据在内存中存储都是以补码的方式存放
  2. 数据在运算的时候是以补码的方式运算
  3. 整数反码补码都是一样的
  4. 补码运算的时候,符号位也参与运算
  5. 运算结果为补码,printf函数打印时,补码还原为原码
  • 原码:数据转换成二进制的样子

  • 反码:在原码的基础上,0变成11变成0,符号位不变

  • 补码: 在反码的基础上加1

  • 一个有符号整数,使用%u方式输出,直接输出补码


  • mian函数外部只用来定义和声明全局变量
  • 全局变量在定义时没有赋值,默认为0
  • 赋值–实际上就是将数据存储变量所在的内存空间
  • =赋值符号的左边称之为左值,指的是变量的内存空间
  • =赋值符号的右边称之为右值,指的是变量的值

1
2
3
4
5
6
#include<stdio.h>
int main() {
	printf("hello, world");
	while(1);
	return 0;
}
  • 编译运行后输出为空

当我们程序运行的时候,将数据输出到屏幕终端,首先数据会先放入一个缓冲区中,然后当达到一定条件时,数据才输出到终端

image.png

满足条件

  1. 遇到换行符时,系统会将这个程序的输出缓冲区的数据冲刷到屏幕终端上
  2. fflush(stdout)刷新缓冲区
  3. 缓冲区满

占位符

占位符 输出类型 扩展
%d 整型 %-{x}d 左对齐x个字符
%f 浮点型 %.{x}f 表示保留小数点后x位
%e 科学计数法 以科学计数法的方式输出浮点数
%o 八进制 %#o 按格式输出
%x 十六进制 %#x 按格式输出
%c 字符
%s 字符串

1
2
3
4
5
#include <stdio.h>
int main() {
	printf("\033[1;31m Hello, World! \033[0m\n"); // 红色文本 
	return 0;
}

  • 原型:int scanf(const char *format,…)
  • 作用:从键盘上获取数据,存储到变量的内存空间中。
  • 参数:const char*format是格式控制字符串,它指定了输入数据的格式。
  • 返回值:返回的是整型数据,代表获取成功的个数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include<stdio.h>

int main() {
	int age;
	float score;
	int ret;
	ret = scanf("%d %f", &age, &score);
	// 获取失败,重新处理
	while(ret < 2) {
		printf("获取失败,请重新输入\n");
		ret = scanf("%d%f", &age, &score);
	}
	printf("ret:%d age:%d score:%f\n", ret, age, score);
}

程序进入死循环,在while循环中scanf函数没有等待输入

  • scanf函数没有正确读取,不会从输入缓冲区拿走数据
  • 在第二次输入前清空缓冲区
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include<stdio.h>

int main() {
	int age;
	float score;
	int ret;
	ret = scanf("%d %f", &age, &score);
	// printf("%d", ret);
	// 获取失败,重新处理
	while(ret < 2) {
		// 清空输入缓冲区
		while (getchar() != '\n');
		printf("获取失败,请重新输入\n");
		ret = scanf("%d%f", &age, &score);
	}
	printf("ret:%d age:%d score:%f\n", ret, age, score);
}

%c会把空格和换行符在内的字符读入,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include<stdio.h>

int main() {
	int age;
	char sex;
	printf("请输入年龄:");
	scanf("%d",&age);
	while(getchar() != '\n');
	printf("请输入性别:");
	scanf(" %c", &sex);
	printf("age:%d sex:%c", age, sex);
	return 0;
}

  • 除法运算时,除数不能为0
  • 整型/整型 结果只能是整型,小数部分直接舍弃,即向下取整
  • 若参与运算的值有一个是浮点数,运算过程中把所有数转化为浮点数,运算结果也是浮点数
  • 取余运算中参与运算的除数只能是整数
  • 自增属于单目运算符,用于一个数的左端或者右端
  • 如果++在后,先参与运算,再自增
  • 如果++在前,先自增,再参与运算
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include<stdio.h>

int main() {
	int x = 2;
	int y = 10 + x++;
	printf("x=%d, y=%d\n", x, y);
	// x=3, y=12
	int z = 10 + (++x);
	printf("x=%d, z=%d\n", x, z);
	// x=4, z=14
	return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include<stdio.h>

int main() {
	int i = 3;
	int ret = i++ + i++; // 3 + 4
	printf("i=%d, ret=%d\n", i, ret);
    // i=5, ret=7
    i = 3;
	ret = i++ + (++i); // 3 + 5
	printf("i=%d, ret=%d\n", i, ret);
    // i=5, ret=8
    return 0;
}
  • 由关系运算符组成的表达式称之为关系表达式
  • 关系表达式的结果只有两种,要么关系成立为1,不成立为0
1
2
3
4
5
6
7
8
9
#include<stdio.h>

int main() {
	int a = 10;
	printf("%d\n", 0<a<2); // 1
	int b = a == 10;
	printf("%d\n", b); // 1
	return 0;
}

由逻辑运算符组成的表达式称之为逻辑表达式,由逻辑运算符组成的表达式称之为逻辑表达式,逻辑表达式的结果是布尔值,要么为非0为真),要么为

  • 逻辑表达式中存在惰性运算(短路特性),如果前面的表达式已经能表达出明确的结果了,那么后面就不会算了。

位运算指将一个数据进行bit位运算,也就是说,要将这个数据转成二进制(补码),然后再进行运算位操作。

  • 按位或|:至少有一位为1,结果就为1
  • 按位与&:两个都为1,结果才为1
  • 按位取反~:把每个bit位的0变为1,1变为0
  • 按位异或^:两个比特位不同为1,相同为0
  • 按位左移«:除符号位,所有的bit位左移x位
  • 按位右移»:除符号位,所有的bit位右移x位

移位运算中如果出现如下情况,则其行为未定义:

  1. 右操作数(即移位数)为负值;
  2. 右操作数大于等于左操作数的位数;

例如,对于 int 类型的变量 a , a<<-1 和 a<<32 都是未定义的。

对于左移操作,需要确保移位后的结果能被原数的类型容纳,否则行为也是未定义的。对一个负数执行左移操作也未定义。

对于右移操作,右侧多余的位将会被舍弃,而左侧较为复杂:对于无符号数,会在左侧补 

data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
0
;而对于有符号数,则会用最高位的数(其实就是符号位,非负数为0 ,负数为 1))补齐

  • 可以使用移位操作计算2的幂次问题
1
2
3
4
5
6
int mulPowerOfTwo(int n, int m) {  // 计算 n*(2^m)
  return n << m;
}
int divPowerOfTwo(int n, int m) {  // 计算 n/(2^m)
  return n >> m;
}
  • 可以使用异或操作交换两个数的值
1
2
3
4
5
void swap(int &a, int &b) {
	a ^= b;
	b ^= a;
	a ^= b;
}
  • 取一个数二进制的某一位
1
2
3
4
// 获取 a 的第 b 位,最低位编号为 0
int getBit(int a, int b) {
	return (a >> b) & 1;
}
  • 将一个数二进制的某一位置为0
1
2
3
4
// 将 a 的第 b 位设置为 0 ,最低位编号为 0
int unsetBit(int a, int b) {
	return a & ~(1 << b);
}
  • 将一个数二进制的某一位置为1
1
2
3
4
// 将 a 的第 b 位设置为 1 ,最低位编号为 0
int setBit(int a, int b) {
	return a | (1 << b);
}
  • 将一个数二进制的某一位取反
1
2
3
4
// 将 a 的第 b 位取反 ,最低位编号为 0
int flapBit(int a, int b) {
	return a ^ (1 << b);
}
  • 取一个数二进制的最后一位1
1
2
3
int getLastOne(int a) {
	return a & (a-1);
}
  • 去掉二进制数最低位的1
1
2
3
int removeLastOne(int a) {
	return a ^ (a & -a);
}
1
2
3
4
5
6
7
if ("表达式") {
	//code
} else if {
	//code
} else {
	//code
}
  • 只要表达式的值为真(非0),执行{ }复合语句 .
  • “表达式”:任意的C语言合法的表达式都可以
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
switch("表达式") {
	case 1:
		//code
		break;
	case 2:
		//code
		break;
	...
	default:
		//code
		break
}
  • “表达式"的值必须为整数值(整型,字符型,枚举)
  • “常量表达式"的值必须为整数值,且每个case后面的常量表达式的值必须不相同。
  • 当表达式的值与某一个case后面的常量表达式的值相同时,就会打开"开关”,执行冒号后面的语句,直到有break语句就提前跳出switch,否则就轮到下一个case,且不会比较常量表达式的值,就直接执行冒号后面的语句(因为开关已经打开了)。当所有的语句都执行完了,就结束了switch。
  • break;提前跳出switch语句,常用的用法是每个case语句后都有一个break语句。
  • 当所有case后面的常量表达式都不匹配时,就会执行default后面的语句,且打开"开关”。

注意:

  • break在这里是退出switch的关键,如果在case当中的执行语句中没有遇到break,它将会一直执行下去,直到遇到break或者到底了
  • default这个可加可不加
  • default放在哪里都不会影响switch的正常使用
  • “判断值”只能是整型数据或者是枚举
  1. goto语句与if构成的循环
  2. while循环
  3. do~while循环
  4. for循环
  • 格式
1
2
3
语句标号:
// code
goto 语句标号; //此时程序将会直接跳转到语句标号的地方开始执行
  • goto 语句标号;

  • “语句标号”:在C语言中,把一个名字(C标识符)与某一行的地址相关联。

  • 格式为在一行的开始处定义一个名字,然后再加一个冒号

  • loop:

1
2
3
n++;

goto loop;

作用:

  1. 与if语句向上跳转构成循环结构
  2. 向后跳转可以跳出多重循环体等语句

注意:

  • goto本身没有任何问题,但是goto语句是无条件跳转语句,应用太灵活,会使程序的可读性变差,应限制使用。
  • goto语句只能够在函数内进行跳转,不可以跨越函数跳转。
  • goto语句一般用于代码出现异常时,直接goto跳转到异常处理的代码处。比如说多层循环嵌套执行时出现异常,则直接跳出。
1
2
3
4
while(表达式)//表达式的值为真(非0)时,进入循环体;为假时,跳出循环体 
{ 
    循环体
}

do while循环的特点:无论判定条件是否为真,至少会执行一遍循环体

1
2
3
do{ 
    循环体;
}while(表达式);
1
2
3
4
5
6
7
for( 初始化语句表达式1; 判断语句表达式2; 循环操作语句表达式3) { 
	循环体
}
 
for(int i=0; i<5; i++) {
    printf("%d\n",i);
}
  • for循环中 的表达式123都可以为空,表达式2为空表示判断条件永远为真。while的表达式不能为空。
  • while循环与for循环的区别:一般来说,while更注重循环条件,for更注重循环次数。 当然它们是可以相互替换的。
1
2
3
4
5
6
7
8
int i,j,a=0; 
for(i=0;i<5;i++){ 
	//外循环 
    for(j=0;j<4;j++) { 
	    //内循环
    	printf("%d %d %d\n",i,j,a++); 
    } 
}
  • break关键字只能用于循环(while for do…while)和switch语句中,表示结束循环或者跳出switch语句
  • break关键字只能跳出最近一层的循环,也就是说,如果有多重循环,只能跳出最靠近break语句那层循环
  • break关键字 不能 单独用于 if语句中,除非if语句外面包含了循环
  1. continue关键字只能用于循环体中(while do…while for),用于提前结束当前循环体后,又从最近一层循环体开始执行
  2. continue关键字不能 单独用于 if语句或者switch 语句中,除非外面包含了循环
1
#define 宏名 宏体
  • 宏名:宏定义的标识符,一般用大写字母表示
  • 宏体:宏定义的字符串,可以是常量、表达式、函数调用等
  • 宏定义的作用:在预处理阶段,将所有的宏名替换成宏体,然后再进行编译

宏定义的注意事项:

  • 宏定义没有类型,宏体可以是任意类型的值
  • 宏定义没有作用域,宏定义的宏名在全局范围内都有效
  • 宏定义是简单的文本替换,不会进行类型检查,可能会导致错误
  • 宏定义可以包含参数,参数可以是任意类型的值
  • 宏定义的参数没有类型,宏体中的参数会被替换成宏体中的值
1
#define MAX(a, b) ((a) > (b) ? (a) : (b))

由于宏定义中的参数可以为任何值,任何符号,参数可能为多条语句,为了宏定义能达到预期的效果,宏定义中的参数需要用括号括起来,否则可能会导致错误。

使用宏定义可以达到正常语法无法达到的效果。

例如

1
2
3
4
5
6
#define Vec(T) \
struct {\
	T *data; \
	int size; \
	int capacity; \
}

在这个例子中,使用宏定义可以实现任意类型的动态数组,注意:在宏定义包含多行的时候,用续行符\进行连接,表明它们为同一行

在linux内核中,还可以经常看到这样的写法, 例如linux内核链表中初始化头结点的宏定义

1
2
3
#define INIT_LIST_HEAD(ptr) do { \
	(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)

在宏定义中,使用do...while(0)包裹起来,表明这些语句为一个整体,可以防止宏定义被误用,如果没有使用,例如在if语句中采用这样的格式

1
2
if (condition)
	INIT_LIST_HEAD(ptr);

那么无论条件成立,宏定义中第二条以及之后的语句都会执行,所以,如果你的宏定义中包含多条语句,那么最好使用do...while(0)包裹起来。

在C语言中,宏定义被大量使用,但是由于宏定义没有类型检查,无法打断点调试,降低代码可读性,所以,在编写宏定义的时候,要格外小心。如果你的逻辑可以使用C语言的语法正常实现,那么请尽量不要使用宏定义。