大学学了c之后,一直没有好好复习过,一些特性和语法都忘了,这里总结下。
一:一些关键字
1:typedf的用法
- 给已知类型取别名
struct Person{
char name[64];
int age;
}
typedf struct Person myPerson;
typedf struct Person{
char name[64];
int age;
}myPerson;
- 同样是给已知类型取别名,但用于指针类型的时候有特殊效果
//p1是char* 类型,p2是char类型
char* p1,p2;
//p1 p2 都是 char* 类型
typedf char* pChar
pChar p1,p2;
//p1 p2 都是 char* 类型
char *p1, *p2;
2:void类型
不能用void定义一个类型,因为编译器不知道分配多少个字节
但是 void* 是可以的,因为所有指针都是四个字节
//定义了一个无类型指针,所有类型指针的祖宗
void* a
void还可以用做 形参 和返回值
//虽然输入和输出都是void,但是这样写,可以明确的定义一个函数(但是c++不需要,如果没有传参,会直接报错)。
void func(void){};
3:预处理命令
这里 IF 0之后的代码是无效的
IF 0
代码
END IF
4:sizeof() 计算一个变量占用内存空间大小
1:返回值要考虑字节对齐的问题
2:返回值是 unsigned int,大部分编译器,使用无符号整型计算的结果也是无符号整型
//这里结果是 4294967286
unsigned int a = 10;
cout << a - 20 << endl;
//但是如果用printf,结果会不一样,结果是-10,因为%d会强行将无符号数变为有符号数
printf("%d",a-20);
数组本身是一个指向头元素的指针,当数组作为参数传递给函数时,计算的是指针的大小,如果不是作为函数参数,计算的是数组本身的大小。
int calArrSize(int arr[]){
return sizeof(arr);
}
int main(){
int a[] = {1,2,3,4,5,6,7};
cout << sizeof(a) << endl; //结果是28
cout << calArrSize(a) << endl; //结果是4
return 0;
}
5:编译、extern、static
头文件不参与编译,在c语言中,每一个.c文件是一个编译单元,每一个编译单元是独立编译的,因此下面的代码不能够成功运行
文件A中
int a = 0;
文件B中
cout << a << endl;
B文件在独立编译时,不知道a变量的存在,因此会报错,如果需要通过编译,就需要使用extern关键字告诉编译器 a是存在的,在之后的链接过程中,链接器就会去找a。
extern int a;
cout << a << endl;
内部连接:变量只能在当前文件内使用
外部连接:变量可以被其他文件使用
本来全局变量是放在静态区的,可以被其他文件所访问,但如果使用 static 修饰全局变量,全局变量就变成了内部链接,只能被当前文件所访问。
所以,如果a确实没有定义,或者是static类型的变量,在链接的时候,还是会报错的。
extern 是单个声明,而头文件则是批量声明(include),编译的时候,会将头文件的内容直接拷贝进来。
6:常量区
(1):const全局和局部变量区别
const全局变量在常量区,常量区的变量都不能修改(直接或间接都不能修改)
const int a = 10;
int main(){
a = 20; //报错,不能直接修改
int* p = &a;
*p = 20;//报错,也不能间接修改
}
const局部变量在栈上,栈上的值都是可以修改的(可以间接修改)
int main(){
const int a = 100;
a = 200; //报错,不能直接修改
int* p = &a;
*p = 200; //可以间接修改
}
(2):字符串常量
字符串常量都在常量区,常量区的变量名就是字符串本身的名字
int main(){
char* p = "Hello world";
printf("%d",&("Hello world"));
printf("%d",p);
}
打印:
4214855
4214855
字符串常量能不能修改,看编译器,一般来说,不允许修改
字符串常量如果值相同,会不会共用一个,这个不同编译器也不一样
7:内存分区总结
总的来说,分为 代码区 和数据区
代码区:存放编译后的二进制代码,不可寻址
数据区:堆、栈、全局 \ 静态、常量(分字符串常量区和变量常量区)
二:指针和函数
1:宏函数
宏函数其实不是函数,类似c++的内联函数,编译的时候替换,由于不会有压栈操作,因此大多数时候比函数效率高
#define ADD(x,y) x+y
int main(){
cout << ADD(1,2) << endl;
}
2:函数调用惯例
惯例指的是函数的调用者和被调用者对函数调用有着一致的理解
(1)函数参数的传递顺序和方式
参数入栈的方式是从左到右还是从右到左?
(2)栈的维护方式
函数出栈的工作是由调用方来完成还是被调用方完成?
一般来说,c语言默认使用 cdecl 惯例,_如果没有手动指定,编译器会自动加上(windows默认stdcall)
int _cdecl func(){};
调用前后的两个函数,调用惯例需要一样。
3:指针
32位下,所有指针都是4个字节,里面存的是内存地址(64位操作系统是8个字节)
指针步长: 指针变量+1时,要向后跳多少字节,不同类型的指针就表现在指针步长不同。
解引用所取字节数 + 指针步长 = 指针类型
4:free使用后要记得把指针置空
free使用后要记得把指针置空
char a[] = "192.168.0.1";
char buf[1024] = {0};
strcpy(buf,a);
free(buf);
// buf = NULL
cout << buf << endl; //缺少buf = NULL,输出还是 192.168.0.1
三:一些c中常用函数
1:计算结构体中成员变量偏移量 offsetof
#include <stddef.h>
cout << offsetof(struct Person, b) << endl;
2:在堆区分配内存 malloc
char* s = malloc(sizeof(char) * 100);
3:初始化 memset
memset(s, 0, 100); //将s指向内存的前100个字节设置为0
4:复制 strcpy
大长度字符串复制给小长度字符串小心内存溢出
strcpy(s,"Hello world")
5:使用占位符拼接字符串 sprintf (以字符串作为输出)
char buf[1024] = {0};
sprintf(buf,"hello%d world",1);
cout << buf << endl;
//输出:hello1 world
6:calloc 函数——分配连续的一段空间,并初始化
calloc(10,sizeof(int))
7:sizeof 函数
int a = 0;
cout << sizeof(a) << endl;
cout << sizeof(int) << endl;
这里两个都是输出 4
8:重新分配空间 realloc
如果原内存地址后续连续空间不够,就会重新分配一块内存,并将原内存内容复制过来
realloc(p, 10000) //p指针指向的内存
9:sscanf 输出匹配字符串
这里 %*d 是忽略整型的意思
int main(){
char a[] = "1234abcd";
char buf[1024] = {0};
sscanf(a,"%*d%s",buf);
cout << buf << endl;
}
int main(){
char a[] = "192.168.0.1";
int num1 = 0, num2 = 0, num3 = 0, num4 = 0;
sscanf(a,"%d.%d.%d.%d",&num1,&num2,&num3,&num4);
printf("%d,%d,%d,%d",num1,num2,num3,num4);
//输出:192,168,0,1
}
如果是 %*s则是忽略字符串,匹配直到空格或者制表符之类的才结束。
占位符的书写和正则表达式很像
%[a-z] 表示a到z的所有字符
%[aBc] 表示aBc中的某个字符
%[^a] 表示除a之外的字符
%[^a-z] 表示除a-z之外的所有字符
四:数组
1:数组在sizeof的时候,不是指针
int a[] = {1,2,3,4}
cout << sizeof(a) << endl; //16
2:数组名是一个常量指针
char a[] = "192.168.0.1";
a = NULL; //报错,a是常量指针
3:给函数传参的时候,数组的长度是不知道的,所以在传参的时候,一定要传长度
void func(int* a,int len){};
4:使用typedf定义数组指针类型
typedef作用于数组
typedef int(ARRAY)[5]
ARRAY a;
ARRAY* p = &a; //p是一个地址,是指向整个数组的指针,因此步长就是数组的大小
定义数组指针(是一个一级指针,但是步长是数组的长度)
typedef int(ARRAY*)[5]
int a[5] = {0};
//下面两句是等价的
ARRAY p = &a;
int(*p2)[5] = &a;
5:二维数组
二维数组也是一个二级指针,步长是指针大小(下面指向的类型是a[3])
int arr[3][3] = {1,2,3,4,5,6,7,8,9};
int(*p)[3] = arr;
五:结构体
1:结构体的一种特殊写法
简单的结构体的写法和 typedef 先不说,来个奇葩的
//定义结构体的同时,定义了一个结构体变量
struct Person{
char name[20];
int age;
}person = {"ttt",28};
2:结构体内存对齐
原因 :方便计算机寻址,计算机一个字节一个字节读数据,速度太慢,所以会一块一块的读,为了避免读取的数据被分割(前后两次读读了同一个变量的不同部分),就有了字节对齐。
对齐规则:
- 第一个数据成员应该放在 offset 为0的地方
- 之后的数据成员应该放在 min(当前成员大小,#pragma pack(n))
整数倍的地方,比如当前成员是int,默认对齐模式是8,则应该放在4的整数倍的位置。 - 最后结构体所占的字节数,应该是最大成员的整数倍,比如最大元素是 double,那么结构体所占字节数应该是8的整数倍。
使用 pragma pack(n) 设置默认对齐模式为n
六:函数指针
函数名是函数的入口地址或者说本质上是一个存了函数地址的指针
定义函数指针:
void func(int a,char b){
cout << "hello world" << endl;
}
int main(){
typedef void(FUNC)(int,char); //定义函数指针类型
//typedef void(*FUNC)(int,char); 也可以直接定义为指针类型
FUNC* p = func; //生成函数指针
p(1,'a');
//(*p)(1,'a'); //解引用也可以,上面写法可以理解为编译器加上了 *
//直接定义一个函数指针
void(*p2)(int,char) =func;
}
七:其他
1:c++中使用c语言
//防止头文件重复包含
#pragma once
//在c++中能够使用c语言函数(c++中包含c语言的头文件会报错,编译的过程不同,比如对函数的修饰等)
#ifdef __cplusplus
extern "C"{
#endif
#ifdef __cplusplus
}
#endif
2:预处理的基本概念
c语言对源程序处理的四个大步骤:预处理、编译、汇编、链接
预处理:# 开头的就是预处理指令
普通宏定义
#define PI 3.14 //宏定义,作用域是当前文件(就算定义在函数里面,作用域也是整个文件)
#undef PI //销毁宏定义,销毁之前可以使用,销毁之后就不能使用了
#define SUM(x,y) (x+y) //宏函数
条件编译
//条件编译 1
#ifdef FLAG
满足条件执行的代码
#else
不满足条件执行的代码
#endif
//条件编译 2
#ifndef FLAG
#else
#endif
//条件编译3
#if 表达式
#else
#endif
打印错误和位置
printf("%s的%d行出错!",__FILE__,__LINE__)
3: sscanf 和 sprintf
这两个可用于字符串和数字之间的相互转换,比c++的api要方便很多。
两个函数的第一个参数都是字符串,第二个参数是模式串,第三个参数 sprintf是数字,sccanf是数字变量地址。
float val = 0.12312312;
char val_c[100];
//数字转字符串
sprintf(val_c, "%.2f", val);
cout << val_c << endl;
//字符串转数字
sscanf(val_c, "%f", &val);
cout << val << endl;