文章目录
笔记
枚举常量
- 定义
enum Color//Color为枚举类型
{ //RED,YELLOW,BLUE为枚举常量
//定义之后默认分别为0,1,2, 也可以自行定义
RED;
YELLOW;
BLUE
};
- 枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
- 枚举的使用
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //err
字符串
'\0'字符串结束标记不算做字符串的长度
- 字符串和指针
char *s1 = "abcdef";
s1表示字符串首字符的地址,打印则从首元素一直打印到\0
*s1表示将首字符地址解引用,即打印第一个字符
字符串函数
- strcat
char * strcat ( char * destination, const char * source );
不能用于将自己追加到自己后面,即 strcat( str, str) 无法使用 - strncat
char * strncat ( char * destination, const char * source, size_t num );
可以用于自身的追加 - strlen
size_t strlen ( const char * str );
- strcpy
char* strcpy(char * destination, const char * source );
- strncpy
char * strncpy ( char * destination, const char * source, size_t num );
如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。 - strcmp
int strcmp ( const char * str1, const char * str2 );
标准规定:
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字 - strncmp
int strncmp ( const char * str1, const char * str2, size_t num );
- strstr
char * strstr ( const char *, const char * );
Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part of str1 - strtok
char * strtok ( char * str, const char * sep );
1.sep参数是个字符串,定义了用作分隔符的字符集合
2.第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
3.strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
4.strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
5.strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
6.如果字符串中不存在更多的标记,则返回 NULL 指针。
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] ="- This, a sample string.";
char * pch;
printf ("Splitting string \"%s\" into tokens:\n",str);
pch = strtok (str," ,.-");
while (pch != NULL)
{
printf ("%s\n",pch);
pch = strtok (NULL, " ,.-");
}
return 0; }
#include <stdio.h>
int main()
{
char *p = "zhangpengwei@bitedu.tech";
const char* sep = ".@";
char arr[30];
char *str = NULL;
strcpy(arr, p);//将数据拷贝一份,处理arr数组的内容
for(str=strtok(arr, sep); str != NULL; str=strtok(NULL, sep))
{
printf("%s\n", str);
}
}
- strerror
char * strerror ( int errnum );//返回错误码,所对应的错误信息。
#include <stdio.h>
#include <string.h>
#include <errno.h>//必须包含的头文件
int main ()
{
FILE * pFile;
pFile = fopen ("unexist.ent","r");
if (pFile == NULL)
printf ("Error opening file unexist.ent: %s\n",strerror(errno)); //会自动将errno转换为对应的错误码,strerror又会自动将错误码转换为错误信息
//errno: Last error number
return 0;
}
- perror
perror("hehe");
hehe: 错误原因
字符分类字符
大小写转换
int tolower ( int c );
int toupper ( int c );
内存函数
- memcpy
void * memcpy ( void * destination, const void * source, size_t num );
1.函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
2.这个函数在遇到 ‘\0’ 的时候并不会停下来。
3.如果source和destination有任何的重叠,复制的结果都是未定义的。 - memmove
void * memmove ( void * destination, const void * source, size_t num );
1. 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
2.如果源空间和目标空间出现重叠,就得使用memmove函数处理。 - memcmp
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
比较从ptr1和ptr2指针开始的num个字节
返回值如下:
转义字符
\ddd // 单纯一个'\'后面可以跟1-3位数字,表示一个八进制数字
\xdd //'\x'后面跟1-2位数字表示一个十六进制数字
整个转义字符表示一位字符长度
数组
- 数组名代表数组首元素的
地址
,但是有两个除外
- sizeof(数组名):代表计算整个数组的大小
- &数组名:取出的是整个数组的地址
- 数组名代表数组首元素,数组名+1,代表下一个元素的地址
但是&数组名,代表的是整个数组的地址,&数组名+1,代表跳过整个数组+1所在的地址。 - 指针数组和数组指针
int *p [10] = {0};//指针数组,指数组中存放着指针即地址
int (*p)[10] = &arr;//p是数组指针,是指向数组地址的指针,和&数组名相同,都表示的是整个数组的地址,所以一般很少用, 一般在二位数组中使用
#include <stdio.h>
void print_arr1(int p[3][5], int row, int col) {
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", p[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*p)[5], int row, int col) {//P是指向二维数组中第一行的数组的指针
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", p[i][j]);//该指针当作二维数组名使用完全没有问题
printf("%d ", *(p[i]+j));
printf("%d ", *(*(p+i)+j);//p为该行数组的地址,p+i以数组为整体跳过, *解引用之后,表示的是该数组首元素的地址,+j表示第j个元素的地址,再*解引用得出值
printf("%d ", arr[i][j]);
//*(p+i) == p[i]
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0; }
- 一维数组传参
#include <stdio.h>
void test(int arr[])//数组传入数组接受,没毛病
{}
void test(int arr[10])//数组大小写不写,写多少都没关系,没有影响
{}
void test(int *arr)//arr也可视为数组首元素地址,所以指针接受,没毛病
{}
void test2(int *arr[20])//指针数组传入,指针数组接受,数组大小可以不写,没问题
{}
void test2(int **arr)//arr2可以视为指针数组首元素的地址,首元素中又保存着地址,所以用**arr没问题
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
- 二维数组传参
void test(int arr[3][5])//ok
{}
void test(int arr[][])//err-最多可以省略行
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//err arr是二维数组首元素也就是第一行-第一行整个数组的指针,不是该数组首元素地址所以不能接受
{}
void test(int* arr[5])//传参数组大小无意义,主要是用指针数组来保存二维数组,可以由三个元素分别保存三行数组的地址,但由于该地址不是数组数组首元素地址而是整个数组的地址,所以单纯指针数组也不能保存二维数组
{}
void test(int (*arr)[5])//
{}
void test(int **arr)//保存结果也是第一行整个数组的指针而不是数组首元素地址所以也不行
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
- 函数指针数组-数组中的元素为函数指针
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b; }
int mul(int a, int b) {
return a*b; }
int div(int a, int b) {
return a / b; }
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //定义函数数组指针,返回类型为int转移表
while (input)
{
printf( "*************************\n" );
printf( " 1:add 2:sub \n" );
printf( " 3:mul 4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "输入操作数:" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);//ret = p[input](x,y)也可以
}
else
printf( "输入有误\n" );
printf( "ret = %d\n", ret);
}
return 0; }
指针
- 指针类型决定了指针进行解引用时,指针能够访问的内存空间大小
- 指针的类型决定了指针向前或者向后走一步有多大(距离)。
- 二级指针传参
void test(char **p) {
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//二级指针可以接受指针数组
return 0; }
- 函数指针
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int test2(int x, int y)
{}
int main()
{
printf("%p\n", test); //函数名本身就代表函数所在的地址
printf("%p\n", &test); //这两个无区别
void (*pfun1)() = test;//pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
int (*p)(int, int) = test2;//p可理解为test2的别名
return 0; }
函数指针case
解析 void (* signal( int , void(*)(int) ) )(int);
void(*)(int)//表示函数指针类型,参数类型是int, 函数返回值类型是void,没有定义指针变量,如果是void(*p)(int)则意味着定义函数指针变量为p
signal(int, void(*)(int)) 意味着函数signal有两个参数,一个int, 一个函数指针类型
然后signal是整个嵌套在void(*)(int)中,说明该函数的返回类型为函数指针类型
化简为
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
- 指向函数指针数组的指针
void test(const char* str) {
printf("%s\n", str);
}
int main()
{
//函数指针pfun
void (*pfun)(const char*) = test;
//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;
//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[10])(const char*) = &pfunArr;
return 0; }
结构体
- 结构体的定义和初始化
struct Point
{
int x;
int y; }p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
- 使用结构体指针访问指向对象的成员
struct Stu
{
char name[20];
int age;
};
void print(struct Stu* ps) {
printf("name = %s age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name = %s age = %d\n", ps->name, ps->age);
}
int main()
{
struct Stu s = { "zhangsan", 20 };
print(&s);//结构体地址传参
return 0;
}
- 结构体对齐规则
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
VS中默认的值为8
Linux中的默认值为4
修改默认对齐数
#include <stdio.h> #pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所
有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- 内存对齐原因
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址
处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器
需要作两次内存访问;而对齐的内存访问仅需要一次访问。 - 总结:结构体的内存对齐是拿空间来换取时间的做法。
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
s1占用空间是6,6个浪费
s2数据配列紧凑,但因为结构体对齐规则三
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
结构体所占用的空间8,浪费两个,
所以让占用空间小的成员尽量集中在一起。
- 结构体传参
struct S {
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s) {
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps) {
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0; }
上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。 原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论: 结构体传参的时候,要传结构体的地址。
位段
- 定义
:+ 所占位数
struct A {
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
- 内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
占用实际空间为47b,空余17b,总共占用8B
- 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位
还是利用,这是不确定的。
所以跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
- 位段的应用
定义Ipv4等计算机网路协议字段长度
联合
-
定义 联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以
联合也叫共用体)。 -
特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有
能力保存最大的那个成员)。
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
结果
9435560
9435560
11223355
由此结果可以判断当前计算机的大小端存储
所以联合体只能一次性用一个成员变量,否则会出现问题
- 联合体的大小
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
8
16
动态内存分配
- 内存分配
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些
存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有
限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。 - 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似
于链表。 - 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
- malloc
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
- free
void free (void* ptr);
free函数用来释放动态开辟的内存。
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
#include <stdio.h>
#include <errno.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
else
{
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d", *(p + i));
}
}
free(p);
p = NULL;//很有必要
return 0;
}
- calloc
void* calloc (size_t num, size_t size);
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
- realloc
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,
我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
void* realloc (void* ptr, size_t size);
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
- realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2:原有空间之后没有足够大的空间
扩展的方法是:在堆空间上另找一个合适大小的连续空间来
使用。这样函数返回的是一个新的内存地址。 由于上述的两种情况,realloc函数的使用就要注意一些。
#include <stdio.h>
int main()
{
int *ptr = malloc(100);
if(ptr != NULL)
{
//业务处理
}
else
{
exit(EXIT_FAILURE);
}
//扩展容量
//代码1
ptr = realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
//申请失败会返回NULL,所以代码2更好
//代码2
int*p = NULL;
p = realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
}
//业务处理
free(ptr);
return 0; }
- 柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
- 定义
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
上述定义在部分编译器中无法使用可以改为
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
- 特点
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
//code1
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4
- 柔性数组的使用
//代码1
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++) {
p->a[i] = i; }
free(p);
//代码2
typedef struct st_type
{
int i;
int *p_a; }type_a;
type_a *p = malloc(sizeof(type_a));
p->i = 100; p->p_a = (int *)malloc(p->i*sizeof(int));
//业务处理
for(i=0; i<100; i++) {
p->p_a[i] = i; }
//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)
文件
文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的
关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
FILE * fopen ( const char * filename, const char * mode );
int fclose ( FILE * stream );
打开方式如下:
/* fopen fclose example */
#include <stdio.h>
int main ()
{
FILE * pFile;
pFile = fopen ("myfile.txt","w");
if (pFile!=NULL)
{
fputs ("fopen example",pFile);
fclose (pFile);
}
return 0; }
文件的顺序读写
scanf/printf :是针对标准输入流/标准输出流的 格式化输入/输出语句
fscanf/fprintf :是针对说有输入流/输出流的 格式化输入/输出语句
sscanf/sprintf: sscanf是从字符串中读取格式化的数据/sprintf是把格式化的数据输出成(储存到)字符串中
文件的随机读写
- fseek
根据文件指针的位置和偏移量来定位文件指针。
int fseek ( FILE * stream, long int offset, int origin );
/* fseek example */
#include <stdio.h>
int main ()
{
FILE * pFile;
pFile = fopen ( "example.txt" , "wb" );
fputs ( "This is an apple." , pFile );
fseek ( pFile , 9 , SEEK_SET );
fputs ( " sam" , pFile );
fclose ( pFile );
return 0; }
- ftell
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
- rewind
void rewind ( FILE * stream );
文件结束的判定
- 先判断文件是否结束
- 文本文件:返回EOF
- 二进制文件 :成功读取文件的返回值(fread的返回值)小于想要读取的值
- 判断是因为读取失败结束还是遇到文件尾结束
- if feof(fp) == ture
- else if ferror(fp) == ture
文本文件
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if(!fp) {
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp)) //先检查是否因为读取失败
puts("I/O error when reading");
else if (feof(fp))//再检查是否遇到EOF
puts("End of file reached successfully");
fclose(fp);
}
二进制文件
#include <stdio.h>
enum { SIZE = 5 };
int main(void) {
double a[SIZE] = {1.0,2.0,3.0,4.0,5.0};
double b = 0.0;
size_t ret_code = 0;
FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof(*a), SIZE, fp); // 写 double 的数组
fclose(fp);
fp = fopen("test.bin","rb");
// 读 double 的数组
while((ret_code = fread(&b, sizeof(double), 1, fp))>=1)
{
printf("%lf\n",b);
}
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
fclose(fp);
fp = NULL; }
调试
- 先F5进入调试,然后点击调试-窗口,查看各种窗口信息,包括但不限于:监视,自动窗口,局部变量,内存,堆栈,反汇编等。
- F11逐语句,F10逐过程.
逐过程是会自动执行函数而不进入函数,或者自动完成一层循环
coding步骤
- 函数参数类型
- assert() : 当()内为假时 ,抛出异常
- 函数返回值类型确定:函数最好有返回值,这样返回值可以在其他函数中嵌套使用
- const :逻辑上不能改变的值用const,使代码更加健壮
const加在 char *之前,则*str不能改变
const加在*之后,str之前,则str不能改变
程序的编译(预处理操作)+链接
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库, 将其需要的函数也链接到程序中。
- 预编译-文本操作
linux下用命令:gcc -E test.c > test.i 将test.c文件预编译存到test.i文件中
- 将
#include
, 头文件的声明替换为头文件的内容 - 用空格替换掉
//
注释 - 用具体的值替换掉
#define
定义的常量
- 编译-将C语言代码翻译成汇编代码
linux下用命令: gcc -S testi > test.s
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
- 汇编-**将汇编代码翻译为二进制代码
linux下用命令:gcc -c test.s > test.o
- 形成符号表
e.g.
符号名 | 地址 |
---|---|
main | 0x12564 |
- 链接
- 合并段表
- 合并符号表和符号表的重定位
预编译过程详解
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
预处理指令
#define
#define name stuff
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define
macro)。
#define name( parament-list ) stuff 其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff中。 注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部 分。
e.g.
#define SQUARE( x ) x * x
SQUARE( 5 );
25
SQUARE(5+1);
11
因为#define是原理是替换,可以用
#define SQUARE( x ) (x )* (x)
#define 替换规则
- 直接替换,替换之后然后再计算
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
- 注意:
- 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#
-把一个宏参数变成对应的字符串
#define PRINT(X) printf("the value "#x" is %d\n", x)
int a = 10;
int b = 20;
PRINT(a)
PRINT(b)
the value a is 10
the value b is 20
##
-可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
#define CAT(X,Y) X##Y
int Class84 = 2019
printf(%d\n",CAT(Class,84));//相当于print("%d\n",Class84);
2019
宏和函数的比较
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
命名规则 | 宏的名字全大写 | 函数名不要全大写 |
e.g.
#define MAX(a, b) ((a)>(b)?(a):(b))//任何类型都可以比较
#undef
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
命令行定义
#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}
命令行的时候定义ARRAY_SIZE(linux下)
gcc -D ARRAY_SIZE=10 programe.c
条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __DEBUG__ //__DEBUG__//__DEBUG_已经定义则执行,否则不执行
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
- 常见条件编译指令
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol) 相当于 #ifdef symbol
#if !defined(symbol) 相当于 #ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
#include - 文件包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。
头文件被包含的方式
- 本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。 - 库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。 - 嵌套包含
因为嵌套包含了头文件,最终的文件中会又重复使用的头文件
解决方法:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
2.#pragma once
程序的运行
- 程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须
由手工安排,也可能是通过可执行代码置入只读内存来完成。 - 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同
时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。 - 终止程序。正常终止main函数;也有可能是意外终止。
部分算法识记
- 交换两数
int main()
{
int a = 3;
int b = 5;
printf("%d,%d", a, b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("交换完成后:%d,%d", a, b);
return 0;
}
常见错误
- scanf函数 丢掉&
&的意思是取该变量地址,将要输入的文件保存在该地址下,而若变量为数组则不需要加&
num1 = 0;
num2 = 0;
scanf("%d%d", &num1,&num2);
- 不同源文件全局变量的使用 需要在使用的源文件中用extern再次声明一下
char str[] = {"a","b","c"};
上述定义字符串儿会出错因为没有结束符 \0
正确定义字符串儿如下:
char str[] = {"a","b","c","\0"};
char str[] = "abc";
- 运算符先后次序,我们只能知道乘法运算在加法运算之前,但加法运算是先运算+左边还是+右边,由不同的编译器定义,所以可移植性差,所以不要在程序中,连续使用复杂的运算次序的多个运算符。
- 如何规避野指针
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 指针使用之前检查有效性
- 结构体传参首选传结构体的地址
原因:函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。 - 内存定义布局导致出错
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<=12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0; }
- 栈区内存开辟空间从高地址到低地址
- 数组存放从低地址到高地址
如下图所示:
变量 | 值 | 地址 |
---|---|---|
i | 0 | 高地址 |
arr[11]的地址 | ||
arr[10]的地址 | ||
arr[9] | 0 | |
… | 0 | |
arr[0] | 0 | 低地址 |
for循环到i = 12 即arr[12]时,地址和内存中i开辟的空间重合,使得i = 0, 循环又从头开始,导致死循环。
- 常见的动态内存错误
- 对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
- 对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
- 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//err
}
- 使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
- 对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
- 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
程序报错
- 类型前缺少;(但实际已经有了;)
错误原因可能是,C变量定义需统一在函数开头,不能在中间定义 - scanf不安全
在文件开头加上#define _CRT_SECURE_NO_WARNINGS 1