数据类型
类型大小
数据类型 | 大小 | 数据类型 | 大小 |
---|---|---|---|
char | 1 | int | 4 |
short | 2 | long int | 8 |
float | 4 | double | 8 |
long long | 8 | long double | 16 |
关键字
register
声明局部变量,通知编译器将该变量储存在寄存器中(通常是内存),可以提高访问速度。一般用于某个局部变量会被多次访问的情况。
auto
声明局部变量,默认省略。在C语言中,只使用auto
修饰变量,则变量类型为int
,C++中会自动推导数据类型。
restrict
用来修饰指针,表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改。(是简易性的,强制再次使用指针对变量进行引用、修改,编译器不会报错,结果也和不用restrict修饰相同)
sizeof
sizeof
在代码进⾏编译的时候,就根据表达式的类型确定了,类型的常用,而表达式的执行却要在程序运行期间才能执行,在编译期间已经将sizeof
处理掉了,所以在运行期间就不会执行表达式了。
int main()
{
short s = 2;
int b = 10;
printf("%d\n", sizeof(s = b+1));
printf("s = %d\n", s);
retrun 0;
}
sizeof(数组名)
会返回整个数组的大小,其余情况下,sizeof(指针)
返回的是4/8。
static
static
修饰局部变量可以改变变量的生命周期,生命周期的改变本质是改变了变量的存储类型。被static
修饰的变量会存储在静态区,存储在静态区的变量和全局变量是一样的,只有程序运行结束才销毁。但是作用域不变。
static
修饰的全局变量具有全局作用域,只初始化一次,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static
关键字修饰过的变量具有文件作用域。(使得作用域仅限于被定义的文件中即,从变量定义到本文件结尾处,其他文件不论通过什么方式都不能访问)。注意:xxx.h
和xxx.hpp
中定义的全局变量即使使用static
修饰,其他文件也可以访问这些变量,只有生命和定义分离的xxx.h
和xxx.cpp
这种写法中,定义在xxx.cpp
中被static
修饰的全局变量才不能被其他文件访问。
static
修饰函数和修饰全局变量相同。
signed和unsigned
char
类型的数据默认可能是signed char
,也可能是unsigned char
,由当前系统决定。
其他类型的数据,默认是signed xxx
。
字符串
printf()和strlen()
遇到'\0'
就会停止。
指针
数组名的理解
只有在sizeof(数组名)
和&数组名
的时候,数组名表示整个数组,其余情况数组名都表示数组的首元素地址
指针数组和数组指针
int *p arr[5]; // 指针数组
int (*p) arr[5]; // 数组指针
函数指针
// 定义变量
int (*padd) (int, int) = Add;
int (*padd) (int, int) = &Add;
// 变量类型
int (*) (iny, int)
// 使用
int sum = padd(x, y);
int sum = (*padd)(x, y);
字符函数
大小写判断和转换
判断
int islower(int c);
int isupper(int c);
转换
int tolower(int c);
int toupper(int c);
strlen的模拟实现
#include <string.h>
size_t strlen(const char* str);
统计字符串中\0
之前的字符个数,函数返回值为size_t
类型。
size_t my_strlen(const char* str)
{
assert(str);
char* cur = str;
while(*cur != '\0')
{
++cur;
}
return cur - str;
}
strcpy的模拟实现
#include <string.h>
char* strcpy(char* dest, const char* src);
char* strncpy(char* dest, const char* src, size_t num);
源字符串必须以\0
结尾,拷贝时也会拷贝\0
。
目标字符串必须有足够的空间,而且目标字符串可以修改。
strncpy
不会自动补充\0
。
如果源字符串的长度小于num
,则拷贝完源字符串之后,在目标的后边追加0
,直到num
个。
// 参数的顺序
// 函数的返回值
// 函数的功能,停止条件
// assert
// const修饰指针
char* my_strcpy(char* dest, const char* src)
{
assert(src != NULL && dest != NULL);
char* ret = dest;
while (*dest++ = *src++)
{
;
}
return ret;
}
strcat的模拟实现
#include <string.h>
char* strcat(char* dest, const char* src);
char* strncat(char* dest, const char* src, size_t num);
源字符串必须以\0
结尾。
目标字符串必须也以\0
结尾,不然不知道从哪里开始追加。
不能自己给自己追加。
strncat
会自动追加\0
。
char* my_strcat(char* dest, const char* src)
{
assert(dest != NULL && src != NULL);
char* ret = dest;
while (*dest)
++dest;
while (*dest++ = *src++)
;
return ret;
}
strcmp的模拟实现
#include <string.h>
int strcmp(const char* str1, const char* str2);
int strncmp(const char* str1, const char* str2, size_t num);
如果str1
大于str2
,则返回一个大于0的数。
如果str1
等于str2
,则返回一个0。
如果str1
小于str2
,则返回一个小于0的数。
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 != NULL && str2 != NULL);
while (*str1 == *str2)
{
if (*str1 == '\0')
return 0;
++str1;
++str2;
}
return *str1 - *str2;
}
strstr的模拟实现
#include <string.h>
char* strstr(const char* str1, const char* str2);
函数返回str2
第一次在str1
中出现的位置。
字符串的比较匹配不包含\0
,以\0
作为结束标识。
char* my_strstr2(const char* str1, const char* str2)
{
assert(str1 != NULL && str2 != NULL);
if (*str2 == '\0')
return (char*)str1;
while (*str1)
{
char* s1 = (char*)str1;
char* s2 = (char*)str2;
while (*s1 && *s2 && *s1 == *s2)
{
++s1;
++s2;
}
if (*s2 == '\0')
return (char*)str1;
++str1;
}
return NULL;
}
strtok的使用
#include <string.h>
char* strtok(char* str, const char* sep);
strtok
会找到str
中的下一个标记,并在其后添加\0
,返回指向这个标记的指针,同时还会将这个标记从str
中删除.
原字符串的改动是切分符原位置均更改为 \0
,所以内容都还在,可以通过逐个字符打印检验。
int main()
{
char str[80] = "This is - www.runoob.com - website";
const char s[2] = "-";
char *token;
/* 获取第一个子字符串 */
token = strtok(str, s);
/* 继续获取其他的子字符串 */
while (token != NULL)
{
printf("%s\n", token);
token = strtok(NULL, s);
}
printf("\n");
for (int i = 0; i < 34;i++)
printf("%c", str[i]);
return (0);
}
内存函数
memcpy的模拟实现
#include <memory.h>
void* memcpy(void* dest, const void* src, size_t num);
memcpy
从src
的位置开始向后复制num
个字节的数据到dest
。
函数在遇到\0
时不会停止。
函数的返回值和str1
相同,所以一般不使用返回值,也无需接收返回值。
如果dest
和src
有重叠,结果未定义。这种情况应该使用memmove
。
void* my_memcpy(void* dest, const void* src, size_t num)
{
assert(dest != NULL && src != NULL);
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
memmove的模拟实现
#include <memory.h>
void* memmove(void* dest, const void* src, size_t num);
和memcpy
的区别在于:memmove
处理的内存块是可以存在重叠的。
void* my_memmove(void* dest, const void* src, size_t num)
{
void* ret = dest;
assert(dest != NULL && src != NULL);
if (dest <= src || dest > (char*)src + num)
{
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else {
dest = (char*)dest + num - 1;
src = (char*)src + num - 1;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest - 1;
src = (char*)src - 1;
}
}
return ret;
}
memset和memcmp的使用
void* memset(void* dest, int value, size_t num);
int memcmp(const void* str1, const void* str2, size_t num);
数据在内存中的存储
大小端
小端模式:数据的高位字节内容存储在高地址处,低位字节内容存储在低地址处。
大端模式:数据的高位字节内容存储在低地址处,低位字节内容存储在高地址处。
结构体
匿名结构体
typedef struct
{
int data;
Node* next;
}Node;
这样定义匿名结构体是错的,因为在typedef
之前,创建结构体的时候,内部就已经使用了Node
这个名称,是错误的。
内存对齐
-
结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
-
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器的默认对齐数 和 该变量自身的大小 的较小值
-
结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
修改默认对齐数
#pragma pack(1); 修改对齐数为1
#pragma pack(); 修改对齐数为默认对齐数
位段
-
位段的成员必须是
int
、unsigned int
或signed int
,在C99中位段成员的类型也可以选择其他类型。 -
位段的成员名后边有⼀个冒号和⼀个数字。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
位段不具有跨平台性。
位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的比特是没有地址的。 所以不能对位段的成员使用&
操作符,这样就不能使用scanf
直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
int main()
{
int a, b;
struct A sa;
scanf("%d %d", &a, &b);
sa._a = a;
sa._b = b;
return 0;
}
联合体
-
联合的大小至少是最大成员的大小。
-
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
动态内存管理
void* malloc(size_t size);
- 开辟一块连续的空间,如果成功则返回空间首地址,如果失败返回
NULL
,所以必须检查返回值。 - 返回值类型是
void*
,所以接收返回值之前要做强制类型转换。
void free(void* ptr);
- 如果
ptr
指向的不是动态开辟的空间,结果是未定义的 - 如果
ptr == NULL
,则什么都不做 - 如果该空间已经被释放,会报错。
- 如果
ptr
指向的是动态开辟的空间的一部分,不是起始位置,会报错。
void* calloc(size_t num, size_t size);
- 为
num
个大小为size
的元素开辟空间,并将所有空间初始化为0
calloc
和malloc
在功能上的区别只有一个:calloc
会对空间做初始化。
void* realloc(void* ptr, size_t size);
- 函数调用有可能失败,失败返回
NULL
。所以不能直接用ptr
接收该函数的返回值,因为如果开辟失败,ptr
更新为NULL
,那么原来ptr
指向的的空间就会造成内存泄露。
柔性数组
定义
C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
struct A
{
int _a;
int arr[]; // 柔性数组成员
};
特点
- 结构中的柔性数组成员前面必须至少一个其他成员。
sizeof
返回的这种结构大小不包括柔性数组的内存。- 包含柔性数组成员的结构用
malloc()
函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
使用
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
A *p = (A*)malloc(sizeof(A)+100*sizeof(int));
//业务处理...
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
总结
柔性数组是可以被指针替换的。
struct A
{
int i;
int *p_a;
};
柔性数组比指针的优势在于:
- 方便内存释放。只需要释放结构体的空间即可,而指针的方案则需要先释放指针指向的空间,再释放结构体的空间。
- 有利于提高访问速度,减少内存碎片。(牵强)
文件操作
文件的打开和关闭
FILE* fopen(const char* filename, const char* mode);
int close(FILE* stream);
mode
表示文件的打开模式,下面是文件的打开模式。
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开⼀个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向⼀个二进制文件尾添加数据 | 建立一个新的文件 |
“r+”(读写) | 为了读和写,打开⼀个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建立⼀个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
文件的顺序读写
顺序读写函数介绍
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件输入流 |
fwrite | 二进制输出 | 文件输出流 |
文件的随机读写
fseek
int fseek(FILE *stream, long int offset, int whence);
offset
是相对于whence
的偏移量,以字节为单位,正值表示向后,负值表示向前。
whence
表示开始添加偏移offset
的位置。它一般指定为下列常量之一:
选项 | 含义 |
---|---|
SEEK_SET | 文件的开头 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件的末尾 |
ftell
long int ftell(FILE *stream);
返回位置标识符的当前值。如果发生错误,则返回 -1
,全局变量errno
被设置为一个正值。
经常配合fseek
来获取文件的大小:
#include <stdio.h>
int main ()
{
FILE *fp;
int len;
fp = fopen("file.txt", "r");
if( fp == NULL )
{
perror ("打开文件错误");
return(-1);
}
fseek(fp, 0, SEEK_END);
len = ftell(fp);
fclose(fp);
printf("file.txt 的总大小 = %d 字节\n", len);
return(0);
}
rewind
void rewind(FILE* stream);
让文件指针的位置回到文件的起始位置。
预处理、编译、汇编、链接
预处理
- 宏替换
- 处理条件编译
- 头文件展开
- 删除注释,注释被替换为一个空格
- 添加行号和文件名标识,方便后续编译器生成调试信息等。
- 或保留所有的
#pragma
的编译器指令,编译器后续会使用。
编译
编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件。
汇编
汇编器是将汇编代码转转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。就是根据汇编指令和机器指令的对照表一一的进行翻译,也不做指令优化。
链接
链接过程主要包括:地址和空间分配,符号决议和重定位等步骤。
预处理详解
预定义符号
__FILE__ // 进行编译的原文件
__LINE__ // 文件当前的行号
__DATE__ // 文件进行编译的日期
__TIME__ // 文件进行编译的时间
__STDC__ // 如果编译器遵循ANSI C,其值为1,否则未定义
宏函数
#define name( parament-list ) stuff
其中的parament-list
是一个由逗号隔开的表达式,它们可能出现在stuff
中。
参数列表的左括号必须与name
紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff
的一部分。
用于对数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
宏替换的规则
- 在调用宏时,首先对参数进行检查,看看是否包含任何由
#define
定义的符号。如果是,它们首先被替换。 - 替换问本随后被插⼊到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由
#define
定义的符号。如果是,就重复上述处理过程。
宏和函数的对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 宏代码会插入到程序中,多次使用会导致代码长度增加 | 函数代码只会存在一份 |
执行速度 | 快 | 有创建、销毁栈帧的开销,相对慢一些 |
操作符优先级 | 仅仅做简单的替换,不确定优先级 | 函数优先计算出结果 |
带有副作用的参数(自增、自减) | 结果不可预测 | 结果可预测 |
参数类型 | 不关心 | 关心 |
调试 | 不可调试 | 可调式 |
递归 | 不支持递归 | 支持递归 |
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type) (type*)malloc(num, sizeof(type))
int main()
{
// 使用时可以以类型做参数
int* p = MALLOC(10, int);
// 预处理替换之后
int* p = (int*)malloc(10, sizeof(int));
return 0;
}
#和##
#
运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。#
运算符所执行的操作可以理解为”字符串化“。
#define PRINT(n) printf("The value of "#n" is %d\n", n);
这个宏在使用时,会把#n
替换为"n"
。
int x = 10;
PRINT(x); // 宏替换为 printf("The value of ""x" is %d\n", n);
// 输出 The value of x is 10
##
可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##
被称为记号粘合。
比如,我们想快速实现以下函数
int int_max(int x, int y)
{
return x>y?x:y;
}
float float_max(float x, float y)
{
return x>yx:y;
}
...
我们可以使用宏替换
// 宏定义
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x > y ? x : y); \
}
// 使用宏快速定义函数
GENERIC_MAX(int)
GENERIC_MAX(float)
...
// 使用函数
int main()
{
int x = 0;
int y = 1;
int max1 = int_max(x, y);
float a = 1.0;
float b = 2.5;
float max2 = float_max(a, b);
printf("%d %f\n", max1, max2);
return 0;
}
undef
#undef NAME // 移除NAME的定义
命令行定义
// Linux环境
gcc -D SIZE=10 main.c