文章目录
前言
本文侧重于编程技巧和细节归纳,故不会过多讲解相关的基础知识。作者能力有限,文章难免有不足和勘误。请读者多多包涵。
一. 编译器和操作系统的影响
因为C语言和操作系统联系密切,故在不同编译器和操作系统中差异很大。如不同数据类型的字节大小,对错误的检测机制等。(本文中用的操作系统为 Windows 10,编译器为Visual Studio 2019)
二. 输入输出函数
2.1 scanf
2.1.1 scanf的结束输入方法
① 遇空格,回车,跳格键时结束。
② 遇宽度结束,即字符串的最大长度。
③ 遇非法输入。
2.1.2 scanf的正则表达式
// 1.限制读取字符的范围
scanf("%[a-z]", s); // 只能读入a-z的字符,读到其他字符就停止
scanf("%[a-z0-9A-Z]", s); // 只能读入0-9a-zA-Z的字符,读到其他字符就停止
scanf("%[key]", s); // 只能读入key这几个字符,读到其他字符就停止
// 2.设置以什么字符结束
scanf("%[^\n]", s); // 只能读入非'\n'的字符,读到'\n'就停止 (常用于读取带空格的字符串)
// 3.丢弃读到的数据
scanf("%*c"); // %*c 表示丢弃读到的字符,不写入
scanf("%*[:]"); // %*[:]表示丢弃':'字符,不写入
// 4.综合应用
scanf("%*[^\n]%*c"); // 清空输入缓冲区。%*[^\n] 表示丢弃所有非'\n'的字符,%*c表示丢弃最后的'\n'字符
2.2 printf
// 1.定义的输出格式
int a = 10; float b = 123456.34; char* s = "abcdefg";
printf("%.9s\n", s); // 输出9个字符,不够时左补空格
printf("%-9s\n", s); // 左对齐
printf("%4.3s\n", s); // 先输出(4减3)1个空格,再输出3个字符
printf("%0.4f%\n", b); // 输出保留4位小数
printf("%12.4f\n",b); // 输出(12减4)8个整数,整数不够时先左补空格,再输出4位小数
// 2.输出分段字符串
int n = 1;
printf("%s\n", "abc\0def\0" + 4 * n);
三. 预处理命令及其高级用法
3.1 #,#@ 和 ##的使用规则
// 1.#@ 加单引号,数字直接转成字符(不能是字符串)
#define ToChar(x) #@x
printf("%c", ToChar(1));
// 2.# 加双引号,转成字符串
#define ToString(x) #x
printf("%s", ToString(1234));
// 3.## 连接多个标签
#define Conn(x, y) x##y
// 3.1 当x,y为字符串时,其作用为连接多个字符串
printf("%s", Conn("x", "y"));
// 3.2 当x,y为整数时,其作用为连接多个整数(返回int类型)
printf("%d", Conn(12,34));
// 3.3 当x,y为标签时,其作用为连接多个标签组成变量名(可以是任何类型的变量名)
int xyter = 100;
printf("%d", Conn(xy,ter));
3.2 预处理的综合应用
// 1.声明函数
#define TYPE(type1,type2) type1##_##type2
TYPE(image,load)(int a, int b)
{
return a + b;
}
printf("%d", image_load(3, 4));
// 2.初始化结构体(类似于构造函数)
typedef struct tagBOOK
{
char s[100];
int i;
}BOOK;
#define InitBOOK(s,i) {#s, i}
BOOK b = InitBOOK(qwe2312, 1234);
// 3.重定义各种运算符(不建议使用)
#define y1 (
#define y2 )
#define and ||
#define or &&
printf y1 "34543" y2;
// 4.宏函数
#define SUMu(a,b)\ // do while(0)型,特点是无返回值,但最后没有逗号
do\
{\
a+b;\
\
}while (0)
// 5.加载静态数据库
#pragma comment(lib,"msimg32.lib")
// 6.防止文件重包含
#ifndef _IMAGE_H_
#define _IMAGE_H_
// 文件代码块
#endif // _IMAGE_H_
// 7.加载巨大的数组
int array[10] ={
#include "num.txt" // #include"X" 的作用是,将X文件中的所有内容展开放入#include所在的一行中
};
num.txt ==> 1,2,3,4,5,6,7,8,9,0
四. 各运算符的使用
4.1 前加和后加运算符
注意:不同编译器对前加和后加运算符的处理是有区别的,但性质是通用的。
// 1.前加和后加运算符的核心性质
a.前加和后加运算,类似于函数调用,他们都有返回值。
b.前加的返回值是值得地址。
c.后加的返回值是值本身, 且后加会产生临时变量。
d.前加的优先级大于后加,故在同一式子中先计算完所有的前加,在计算后加。
// 2.例子
int a[5] = {1,2,3,4,5}; int k = 0;
a[++k] = a[++k]; // 12345 // 在vs中结果不变
a[++k] = a[k++]; // 12245 // 在vs中结果不变
a[k++] = a[k++]; // 22345 // 在vs中结果不变
a[k++] = a[++k]; // 32345 // 在vs中结果不变
++k + ++k = 4 // 定理c
(++k + ++k + k++) = (k++ + ++k + ++k); // 定理d
printf("%d%d%d",++k,++k,++k); // 333 // 与函数参数的堆栈操作有关
printf("%d%d%d",k++,k++,k++); // 210 // 与函数参数的堆栈操作有关
// 注意:以上一连串的连加,是未定义行为,无法正确翻译成汇编代码,所以在vs中对该行为进行了处理,在实际开发中慎用
4.2 位移和逻辑运算符
// 1.交换两个数
a ^= b;
b ^= a;
a ^= b; \\注意:判断交换的条件中不能包含等于
// 2.取相反数
a == ~a + 1
// 3.判奇偶数
num & 1 == 1 \\为奇数
num & 1 == 0 \\为偶数
4.3 三元运算符
int a = 2 > 1 ? 10 : 20; // 单个三元运算符与单个if else的处理速度差不多。
int a = 2 > 1 ? 3 : 10 > 8 ? 10 : 100; // 长式子,在后端连接
五. 编程结构和语句
5.1 switch case 语句的本质
1.选择结构
(1).switch case语句
int a = 0;
switch (a)
{
case 1:
a = 10;
case 2:
a = 20;
default:
a = 40;
break;
}
// 反汇编的前半部(后半部是赋值给a赋值)
mov eax,dword ptr [a]
mov dword ptr [ebp-0D0h],eax
cmp dword ptr [ebp-0D0h],1
je __$EncStackInitStart+3Dh (0A825B9h)
cmp dword ptr [ebp-0D0h],2
je __$EncStackInitStart+44h (0A825C0h)
jmp __$EncStackInitStart+4Bh (0A825C7h)
// 由反汇编代码可知,switch case 语句的本质是,先逐个的去判断条件,一满足条件直接case跳转,并一直运行运行到底,或遇到break跳出switch语句。
// 注意:switch case 语句,条件和结果的汇编代码是分开的。if else语句,条件和结果的汇编代码的分开是一一对应,混在一起的。
5.2 for 和 while 语句的区别
// for循环
1.for循环,开头可以声明局部变量,一般用于嵌套循环,且循环次数已知
// while循环
1.while循环,开头不能可以声明局部变量,一般用于循环次数未知的情况,且后续会用到局部变量(循环次数)
5.3 跳转语句
// 注意:一般跳转语句一句占一整行
break // 用于跳出switch,for,while结构,且一次只能跳一层
continue // 用于循环结构中,不再运行continue以下的语句,跳过当次循环,进入下次循环
return // 用于停止所在函数的运行,不再运行return以下的语句,并返回指定的值
goto // 用于跳转到指定的标签位置,多用于多层嵌套结构的跳出问题中
exit(); // 用于结束所运行的exe程序
5.4 C99复合文字
注意:复合文字的用法类似于强制类型转换
typedef struct
{
int a;
char b;
}NUM;
void snn(NUM* n) { printf("%d %c", n->a, n->b); }
NUM* num1 = &(NUM) { 10, '#' }; // 声明结构体指针
snn(&(NUM) { 20, '@' }); // 调用函数
int *a = (int[]){ 1, 23, 45, 6 }; // 声明数组指针
六. 数组和字符串
6.1 数组
// 1.指定数组下标位置进行赋值
int a[10] = { [7] = 10, [2] = 3 };
printf("%d %d\n", a[2], *(a + 7)); // 一个用下标读取,一个用指针读取
// 2.数组的取值,倒取值
int a[5] = { 1,2,3,4,5 };
int* end_pa = &a + 1; // 取a[4 + 1]的位置,即最后的位置,加1加的是整个数组的长度
int* top_pa = a; // 取a[0]的位置,即第一的位置
printf("%d %d\n", end_pa[-1], top_pa[1]);
// 3.扩展数组的含义(在解决实际问题时可极大的简化代码)
// 3.1 以ascll字符码为下标的数组
int a[256(ascll)] = {.....};
printf("%d", a['g']);
// 3.2 同时保存字符串和整形的数组
char* s[2][2] = { {12,"qwe"},{24,"asd"} };
printf("%d %s %c", (int)s[0][0], s[1][1], s[1][1][2]);
6.2 字符串
6.2.1字符串变量和字符串常量
char s1[5] = "1234"; // 字符串数组属于变量,在堆上分配空间,且只能读入length-1个字符,最后一个是'\0'
char* s2 = "qwer"; // 直接定义的字符串属于常量,也在栈上分配空间,但只能进行只读操作
6.2.2 常用的字符串个处理函数
// 1.strlen 计算指定的字符串的长度
size_t strlen(const char *s);
strlen计算的长度是不包括结束字符'\0',当s是字符串数组时其值为length-1(因为最后的'\0'未算在内)
// 2.strcat 合并两个字符串
char* strcat(char* s1 ,const char* s2);
a.合并s1和s2保存到s1中,并返回s1的指针
b.s1必须为字符串数组
c.strcap最后会补'\0',所以s1要足够大
// 3.strcap 复制字符串(多用于给结构体中字符串赋值或进行替换)
char *strcpy(char *s1, const char *s2)
将s2复制到s1中,并返回一个指向s1的新指针
// 4.strncpy 截取字符串
char *strncpy(char *s1, const char *s2, size_t n)
a.截取s2中的n个字符并保存到s1中,并返回s1的指针
b.s1必须为字符串数组,且必须初始化,strncap不会在最后补'\0'
c.默认位置截取是从s2[0]开始到s2[n-1],可通过strncpy(s1, s2 + m, n);改变截取的起始位置
// 5.strcmp 比较两个字符串
int strcmp(const char* s1, const char* s2);
根据ASCII码表比较s1和s2字符串的大小,s1 == s2 返回0,s1 > s2 返回1,s1 < s2 返回-1
// 6.strchr 和 strrchr 查找字符串中指定的字符,返回该字符在内存中的地址,无则返回NULL
char* strchr(const char* str, char c); // 从前向后查找
char* strrchr(const char* str, char c); // 从后向前查找
// 注意:(char*)查 — (char*)原 == 开头到查找位置的字符串长度
// 7.sprintf 将各种数据连接成字符串并保存,返回字符串的长度
int sprintf(char *str, const char *format, ...)
// 8.sscanf 从指定字符串中,格式化获取各种数据,并保存在各变量中
int sscanf(const char *str, const char *format, ...)
七. 函数
7.1 值传递和指针传递的区别
// 值传递:形参是实参的备份,改变形参不能改变实参本身,且值传递参数数据较大,但可返回局部变量的值
// 指针传递:可通过解引用改变原值,可定义任何类型的参数传递(数组,函数等),且参数数据较小,但不能返回局部变量的指针
7.2 可变参数函数
// 1.使用头文件实现可变参函数
#include <stdarg.h> // 实现可变参数必须包含该头文件
int sum(int a, ...)
{
int reNum = 0;
va_list list; // 声明参数列表
va_start(list, a); // 创建参数列表
for (int i = 0; i < a; i++)
reNum += va_arg(list, int); // 使用参数列表
va_end(list); // 删除参数列表
return reNum;
}
printf("%d\n", sum(4, 1, 2, 3, 4));
// 2.使用__VA_ARGS__宏实现可变参宏函数
#define PRINTF(format, ...) printf(#format, __VA_ARGS__)
PRINTF(%d\n, sum(4, 1, 2, 3, 4));
八. 结构体
// 结构体的对齐
typedef struct typedef struct
{ {
char a; int a;
int b; char b;
char c; char c;
}BOOK; }BOOK;
sizeof(BOOK) == 12 sizeof(BOOK) == 8
// 结构体的大小为结构体中最大元素的整数倍,同时也要满足4的倍数,不足时在末尾补全。
// 综上:定义结构体时一般将最大元素放在开头,或结尾。
九. 指针
// 1.指针数组(数组中保存的元素是指针)
char* s[2] = { "qwewq", "weqw" }; // 字符串数组
int** a = (int**)malloc(sizeof(int*)*10); // 动态申请int*数族,注意区分不是二维指针和数组指针
// 2.数组指针(指针指向的是数组)
int(*p)[4] = (int(*)[4])malloc(sizeof(int) * 7 * 4); // 动态申请二维数组,注意行数定义为4
int a[4] = {1, 3, 5, 6}
int(*p)[4] = &a; // 如果数组指指向一维数组则要加&号
// 3.指针函数(函数的返回值是指针)
int* ReturnAarray(int a); // 返回值是整形指针
int (*ReturnAarray())[5]; // 返回值是数组指针
void (*ReturnFunction(int a, int b))(char, int); // 返回值是 void fun(char, int) 型的函数指针
// 4.函数指针(指针指向的是函数)
// 函数指针数组
typedef void (*EVENT)(); // 定义EVENT函数指针数组
enum FUNCTION { PRINTF = 1, SCANF = 0 }; // 用枚举便于管理
int main()
{
EVENT Event[2]; // 在声明时定义函数指针数组的大小
Event[SCANF] = printf;
Event[PRINTF] = scanf;
int a = 0;
Event[PRINTF]("%d", &a); // 使用数组中的函数
Event[SCANF]("%d", a);
return 0;
}
// 5.二维指针
int a1 = 123, a2 = 456, a3 = 789;
int *num1 = &a1, *num2 = &a2;
void UseNum(int **n1, int** n2)
{
*n1 = &a3; // 一层解引用,改变原指针的指向
**n2 = 12345; // 两层解引用,改变原数值
}
UseNum(&num1, &num2);
printf("%d %d", *num1, *num2);
// 注意:NULL指针无法进行两层解引用
十. 动态内存分配
// 1.malloc 申请指定大小的内存空间(不会对内存空间进行初始化)
void* malloc(size_t size);
struct* book = (struct* book)malloc(sizeof(struct book) * 10); \\申请结构体数组
// 2.calloc 申请指定大小的内存空间并初始化为0
void *calloc(size_t nmemb,size_t size);
struct* book = (struct* book)calloc(sizeof(struct book) , 10); \\申请结构体数组
// 3.memset初始化内存空间(该函数在string.h文件中,多用于初始化字符串,数组,结构体)
void* memset(void* s, int c, size_t n);
memset(array,-1,sizeof(array)); // 用-1初始化数组
// 注意:memset是以一个字节为单位初始化指定的内存空间,所以只能初始化赋值为-1或0, 且'\0'== 0
// 4.free 释放指定内存空间(只能释放动态申请的内存空间)
void free(void *ptr);
十一. 文件的读取
11.1 文件路径的处理
// 1.获取运行程序的文件路径
TCHAR exeFilePath[260] = { 0 }; // 注意是TCHAR类型
GetModuleFileName(NULL, exeFilePath, 260);
// 转化成char类型便于后续的处理
char exeFilePath_char[260] = { 0 };
for (int i = 0; i < 260; i++) exeFilePath_char[i] = (char)exeFilePath[i];
printf("%s", exeFilePath_char);
// 2.获取文件后缀名
char* lastName = strrchr(filePath, '.');
// 3.截取文件路径(一般截取开头到最后一个'\')
char* usefile1 = strrchr(filePath, '\\');
char* usefile2 = strncpy((char[260]) { 0 }, filePath, usefile1 - filePath + 1);
printf("%s\n%s\n%s\n", filePath, usefile1, usefile2);
// 4.连接或修改文件路径
char filePath[MAX_PATH]; // MAX_PAT==260
sprintf(filePath, "%s_%s", exePath, ".txt");
11.2 读取文本文件
注意:读取文本文件的核心是,系统会自动以空格和回车作为分隔符
// 1.标准代码框架
FILE* fp; // 创建文件结构体
if ((fp = fopen("map.txt", "r"或"w")) == NULL) // 打开指定文件
printf("file\n");
// 读写操作的代码段.....
fclose(fp); // 释放文件结构体
// 2.读取操作
// 2.1读取数组
int num[10] = { 0 };
for (int i = 0; !feof(fp); i++) // feof 用于检测是否读到整个文本的末尾,是则返回0,否则返回非0
fscanf(fp, "%d", &num[i]); // fscanf 通过指定格式读取数据
// 2.2用正则表达式读取
fscanf(fp, "%d %*[^:]%*c%[^\n]", &num, str); // %*[^:]%*c%[^\n] 表示读取':'后面的内容
// 3.写入操作("w"为重写全文,"r+"为覆盖指定位置的内容,"a"为文本末尾追加)
fprintf(fp, "%s", write);
// 4.重要函数
// 4.1 ftell 获取光标的当前位置
long ftell(FILE *_Stream);
// 4.2 fseek 移动光标到指定位置,成功返回0,否则返回非0
int fseek(FILE* stream, long int offset, int whence)
//whence: SEEK_CUR 当前位置,SEEK_END 末尾位置,SEEK_SET 开头位置
//后补:文件的读和写操作一般是分开处理的,不会混写在一起
11.3 读取二进制文件
// 1.标准代码框架
FILE* fp; // 创建文件结构体
if ((fp = fopen("map.txt", "rb"或"wb")) == NULL) // 打开指定文件
printf("file\n");
// 读写操作的代码段.....
fclose(fp); // 释放文件结构体
// 2.写入操作(以结构体的读写为例)
typedef struct tagBOOK
{
char name[100];
int ID;
float price;
}BOOK;
BOOK book = { "qwer",123,34.56 };
fwrite(&book, sizeof(BOOK), 1, fp); // 元素指针,元素大小,元素数量,文件结构体指针
// 3.读取操作
fread(&book, sizeof(BOOK), 1, fp); // 元素指针,元素大小,元素数量,文件结构体指针
// 补.在保存结构体数组时改变的是元素的大小,而不是元素的数量,即直接保存结构体数组
十二. C相关的其它常用知识
12.1 生成随机数
#include<stdlib.h>
srand((unsigned int)time(NULL)); // 加在每个rand函数前面
int num = rand() % n; // 随机生成0~n-1的整数
12.2 获取系统时间
#include<time.h>
time_t timep;
struct tm* tp;
time(&timep); // 获取1970-01-01至今的小时数
tp = localtime(&timep); // 将time_t的值分解为tm结构,并用本地时区表示
printf("%d/%d/%d %02d:%02d:%02d\n", 1900 + tp->tm_year, 1 + tp->tm_mon, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec);
12.3 调用DOS命令
#include<stdlib.h>
system("cls");
12.4 内联汇编
// 调用printf函数
char* s = "%d";
int num = 100;
_asm // 内联汇编的关键字
{
mov num, 300
push num
mov eax, s
push eax
call printf
add esp, 8
}
十三. 常用的WIN32API
WIN32API 编程是属于操作系统相关的另一体系的知识,且限于文章篇幅和主题,因此在下文中只是给出相应的代码,不做过多的讲解。
13.1 控制台
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &(CONSOLE_CURSOR_INFO) { 1, 0 }); // 隐藏光标,防止打印时闪屏
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), (COORD) { 0, 0 }); // 改变光标位置
system("color F0 "/*F背景色,0前景色*/); \\改变整个控制台的背景色和前景色
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), BACKGROUND_BLUE); // 改变输入时字符的背景色和前景色。 BACKGROUND_BLUE(背景色) FOREGROUND_BLUE(前景色)
// 例子
int color[2] = { BACKGROUND_BLUE ,BACKGROUND_RED }, a = 0;
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &(CONSOLE_CURSOR_INFO) { 1, 0 });
system("color F0 "/*F背景色,0前景色*/);
while(1)
{
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), (COORD) { 0, 0 }); \\改变光标位置
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color[a]);
printf("%s", "⬛⬛⬛⬛⬛⬛⬛\0⬛⬛⬛⬛\0" + a * 11);
a ^= 1;
}
补.1. printf("⬛");一个完整正方形要两个空格
2. 0=黑色,1=蓝色,2=绿色,3=湖蓝色,4=红色,5=紫色,6=黄色,7=白色,8=灰色,9=淡蓝色,A=淡绿色,B=淡浅绿色,C=淡红色,D=淡紫色,E=淡黄色,F=亮白色
13.2 控制鼠标键盘
13.2 .1 C原生的非拥塞控制
#include<conio.h>
if (_kbhit()) // 有按键按下则返回非0,否则返回0
KeyBoard(); // 自定义读取处理函数
13.2 .2 WIN32API对鼠标键盘的处理
// 1.设置鼠标位置 (比例坐标)
SetCursorPos(x, y); // 用的是屏幕坐标系,与窗口坐标系无关
// 2.获得鼠标坐标(比例坐标)
POINT p;
GetCursorPos(&p); // 注意显示设置,中的缩放与布局的百分比,对坐标的影响,100%==屏幕分辨率坐标
// SetPixel 函数用的是比例坐标,所以要进行转换
// 3.控制鼠标按键
mouse_event (MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 ) // 后面四个参数是保留值,写0即可
mouse_event (MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0 )
或 mouse_event (MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 )
// 4.控制键盘按键
keybd_event(49, 0, 0, 0); // 49==A键,中间两位是保留值,最后一位0表示,按键按下
keybd_event(49, 0, 0, KEYEVENTF_KEYUP);
// 5.检测鼠标和键盘的状态
SHORT GetAsyncKeyState(int vKey); // 该函数会改变的返回值的最后一位(第16位)和第一位的值,所以该函数有两种使用方法,用于判断不同的情况
if (GetAsyncKeyState(VK_LBUTTON) & 0x8000) // 按住是1,不按是0,会一下发出多消息,可模拟长按!!!!
printf("ok");
if (GetAsyncKeyState(VK_LBUTTON) & 0x01) // 按下是1,抬起是0,一次发一条消息)
printf("ok");
// 补.各按键的编号:数字 0 - 9==0x30 - 0x39 字母 A(a) - Z(z)==0x41 - 0x5A (注意要写十六进制)
// 补.检测鼠标时的参数为:VK_RBUTTON(右键),VK_MBUTTON(中键),VK_LBUTTON(左键)
// 注意:以上代码有一定的危险性请谨慎使用
// 补充:GetSystemMetrics() 获取设备相关的各种数据(与窗口无关),如鼠标在屏幕上的位置
13.3 多线程编程
void FirstThread()
{
while(1) printf("FirstThread\n");
}
HANDLE hThread = (HANDLE)_beginthreadex((void*)NULL, (unsigned)0, (void*)FirstThread, (void*)0, (unsigned)0, (unsigned*)NULL);
十四. 附件
14.1 ASCII码表
14.2 文件读取模式
十五. 结语
1.本文对C语言知识整理并不全面和详细,有些知识点也是选了其重要的常用的部分经行介绍。后续作者可能会对其进行补充。
2.学习汇编可以更深入的了解C语言乃至其他语言的运行过程。
作者:墨尘_MO
时间:2022年3月9日