*++p、*(++P)
:地址先增再取内容;
*p++、*(P++)
:先取内容再增地址
(*p)++
:内容+1
指针变量自增一次的量和其指向的数据类型有关,若指向的是一个int,地址每次增加一个int型大小的空间,若指向的是一个int数组,则每次增加该int型数组的大小。
int arr[10];
数组名arr表示数组第一个元素的地址,arr、&arr打印出的地址是一样的,&arr表示该数组的首地址
arr+1:地址增加4个字节单位;&arr+1:地址增加4*10个字节单位;
给函数传一个数组参数,数组会退化成指针,失去长度特性;sizeof是操作符,不会使数组退化为指针;strlen获得的是字符串的长度,不包括’\0’,使用strlen配合malloc申请堆空间的时候要记得长度加1.
在使用字符串复制、拷贝、比较函数时,要使用有长度参数的函数,以免内存溢出。
int a=10;
int表示申请一个int大小的内存,a表示该固定内存的名字,a不是地址也不是内容,只是一个名字。
全局变量尽量不要用,不利于多线程;申请的栈空间一定要初始化为0,使用过后的缓冲区也要置0,以免地址拷贝出错(memset)。
常量和静态变量都在全局区,生命周期就是程序的生命周期。若在两个子函数中的两个指针分别指向一样的常量,则两个指针指向同一个全局区中的静态区。不能给null指针和野指针进行内存拷贝,这是非法的。
在子函数中改变父函数中的变量嘚使用上一级的变量来修改,普通变量使用一级指针修改,一级指针使用二级指针修改。不管是开辟普通变量,还是指针变量,亦或是堆空间,我们都要初始化,这是一个好习惯。
//strlen 求字符串长度 不包括'\0'
char str1[] = "abcd"; //sizeof:5;strlen:4
char str2[] = {'a', 'b', 'c', 'd', '\0'};//sizeof:5;strlen:4
char str3[128] = { 'x', 'y', 'z' };//默认填充0;sizeof:128;strlen:3
char *str4 = "abcd"; //sizeof:5;strlen:4;这个语句表示指针指向一个字符串常量
--
数组名和指针的区别:
数组名不能自增访问数据,而指针可以通过自增访问数据;数组名+位移量访问数据比指针自增来访问数据效率更高;数组名之所以不能自增,是应为操作系统要根据数组名释放该数组空间,而多个指针可以指向同一块堆空间,堆空间有用户自己释放;
一个传入指针的函数要用临时变量指向传入指针指向的内存,以免改变原指针的指向;
要判断传入指针是否为空,以免指针越界;
函数的出口尽量少,是程序易于阅读。
0==NULL=='\0';
不要对空指针进行内存的操作,不要让指针越界,一定要检查指针NULL和’\0’的极端情况。
printf可能会被程序屏蔽,而fprintf(stderr,"",)
一般不会被屏蔽。
const修饰的普通变量不能直接写,但是可以通过指针间接的实现写的功能或者强行转化再赋值;宏定义的常量不能参加调试;const在修饰函数的参数时的意义是提高代码的可读性(表明输入输出参数),减少不过。
void const_test(const int *a/* 只读的 输入参数*/)
{
*((int*)a) = 10; //第一点const 在c语言中毫无地位, 第二点 C语言中强转可以任意转换
}
C语言中的强转无敌,const就是弱鸡。
在对二位数组和指针数组进行内容的排序时,后者效率更高,二位数组的排序需要交换数组的内容,而指针数组只要改变指针的指向,这样的效率明显提高。
二维数组只能这样排序:用数组指针来接,其中的6表示p每次偏移6*1个字节
int inverse(char (*p)[6],int n)
//int inverse(char p[][6],int n)
{
int i=0,j=0;
char buf[6]={0};
for(i=0;i1;i++)
{
for(j=i+1;jif(strcmp(p[i],p[j])<0)
{
//只能进行内容的交换,不能直接对一维数组进行交换->p[1]=p[2]等
strcpy(buf,p[i]);
strcpy(p[i],p[j]);
strcpy(p[j],buf);
}
}
}
}
传入指针数组:用二维指针来接
char *arr[5]={"12","34","56","78","90"};
int inverse1(char* p[],int n)//默认每次偏移四个单位地址
//int inverse1(char** p,int n)//这个方式与上面的等价,因为指针数组每个元素的大小为四个字节,而二维指针的每一次偏移也是四个字节。
{
int i=0,j=0;
char *buf=NULL;
for(i=0;i1;i++)
{
for(j=i+1;jif(strcmp(p[i],p[j])<0)
{
//由于每一个数组元素都是指针,既可以通过交换内容来进行排序,也可以通过改变指针的指向来进行排序。
buf=p[i];
p[i]=p[j];
p[j]=buf;
}
}
}
}
当二维数组传递到函数中后,不能改变二维数组元素,即一维数组的指向:
error: assignment to expression with array type|
数组是一种数据类型,那么其就可以取别名:
typedef int (ARRAY_INT_10) [10]; //为 int[10] 这种数组 起一个别名 ARRAY_INT_10
ARRAY_INT_10 b_array; //int b_array[10];
ARRAY_INT_10 * p = &b_array;//定义一个数组指针,即指向数组的指针;
//等价于int (*a)[10] = &b_array;//数组指针
int *p[10];//指针数组,每一个元素都是指针
typedef int(*ARRAY_CHAR_4_POINTER)[4]; //定义一个数组指针类型的别名
ARRAY_CHAR_4_POINTER array_pointer = &array;//int array[4]={0};
int(*array_p)[4] = NULL; //直接定义一个数组指针,array_p属于一级指针类
&array代表整个数组的地址,&array[0]==array==(array+0)代表第一个元素的地址,这两者的地址正好相等,但是意义不同,不是一个类型。
int arr[3][4];
arr==&arr[0]表示指向二维数组的第一个元素,二维数组的元素是一维数组,arr。
*(arr+i)表示二维数组第i个元素的首地址地址,即第i个一维数组的首地址,arr[i]。
((arr+i)+j)表示第第i个一维数组的第j个元素的内容,arr[i][j]。
传入参数退化、修改的问题:
int **p->int ***p
int arr[3]->int *arr[]==int *arr + int n//指针数组的元素大小为4字节,二维指针一次位移为4字节,数组指针一次位移也是4字节。
int arr[3][4]->int arr[][4]==int (*arr)[4] + int n
C语言默认对齐宽度是8个字节:#pragma pack(8) //有1、2、4、8、16
结构体成员的内存分配规律是这样的:
从结构体的首地址开始向后依次为每个成员寻找第一个满足条件的首地址x,该条件是x % N(成员的字节数) = 0,并且整个结构的长度必须为min{成员中最大的那个值,系统设定值:例如#pragma pack(8)}的最小整数倍,不够就补空字节。
当结构体成员为数组时,并不是将整个数组当成一个成员来对待,而是将数组的每个元素当一个成员来分配,其他分配规则不变.
有位段时的对齐规则是这样:同类型的、相邻的可连续在一个类型的存储空间中存放的位段成员作为一个该类型的成员变量来对待,当该类型的长度不够用时,就另起一个该类型长度的存储空间(先使用前面没用完的位),不是同类型的、相邻的位段成员,分别当作一个单独得该类型的成员来对待,分配一个完整的类型空间,其长度为该类型的长度,其他成员的分配规则不变,仍然按照前述的对齐规则进行。
位移操作:无符号左、右移都是补0;有符号左移补0,右移高位补符号位。有符号数左、右移,符号位不变。
二进制文件和文本文件的区别:
由两者的特性可知,只有当要存储的数据是数字的时候才有明显的区别,当一个int型的数以文本的方式存储的时候,存储的是每个数组和符号的ASCII,当以二进制方式存储的时候,存储的是数字对应的二进制码。还有一个要注意的是,windows平台存储和读取文本文件的’\n’时和linux等其他的平台不同,windows:存’\n’->’\r\n’;读’\r\n’->’\n’。linux:存’\n’->’\n’;读’\n’->’\n’。
文件操作模式:
“r” 打开,只读,文件必须已经存在。
“w” 只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)为0字节。再重新写,也就是替换掉原来的文件内容文件指针指到头。
“a” 只能在文件末尾追加数据,如果文件不存在则创建
注意:诸如’b’的格式表示操作的是二进制文本;诸如’+’的格式表示增加了另一种没有的属性(读|写),这种属性的使用要在原有属性可以实现的前提下,才可以真正的具备;例如:’rb+’表示在二进制文件存在且可读的情况下,具有对二进制文件写的属性。
fgetc、fputc每次读写一个字符:’\0’、’\n’等。fgetc遇到错误或者文件尾的时候返回EOF
fputs、fgets每次读写一行,即以’\n’为结束符,存储一行时去掉’\0’,读取一行时添加’\0’。
fputs成功写入返回非负数,失败返回EOF;fgets成功返回一个指向字符的指针,失败或者到达文件尾返回null。
fgets可以把’\n’读进来,通过’\n’区分一行。
fputs可以把’\n’写出去,通过’\n’新开一行
fwrite、fread用来读写二进制文件。
结构体自我包含将会出错:
struct teacher {
int id;
struct teacher t1; //循环定义 编译器最终确定不来 structteacher 到底占用多少个字节
};
静态链表的定义:
struct teacher {
int id;
struct teacher *tp;
};
struct teacher t2;
t2.id = 10;
struct teacher t1;
t1.id = 20;
t2.tp = &t1; // t2->tp = &t1
注意:对于任何传入函数的指针、操作的指针对象我们都要他们是否为空。
注意:在创建一个新的节点时,我们要把节点的next指针置为null,这是一个好的习惯。
①无头链表的初始化、销毁要传二级指针,因为我们要改变一级指针变量。
②无头链表的特点需要对head是否为空进行判断,因为初始化的时候没有分配内存空间给链表。
//找到这个last_node,last_node->next = new_node;
for (last_node = head; last_node->next != NULL; last_node = last_node->next);
③无论是将新的节点插入链表的头部还是尾部,传入的链表指针都得是二级指针。插入链表尾部时,防止链表是空链表;插入链表首部时,我们需要修改链表指针。
④对具有输出功能的传入参数我们一定要最后修改他们,增加函数的的可靠新。
⑤对于单链表,删除一个节点的时候,我们要使用(p->next == del_node)这种形式去查找删除的节点,这样便于删除指定的节点。
⑥在操作的时候传入节点的指针时,我们只要使用指针变量进行比较。
⑦在对堆内存空间进行销毁的时候,我们要判断指向堆的指针是否为空。
以下都是带头链表与无头链表的的区别:
①带头的链表,在初始化的时候我们要分配一个节点的内存空间给链表。
②带头的链表,在进行插入和删除的时候传入一级指针即可,因为无需对链头指针进行修改。
③带头的链表,在进行遍历的时候,第一个元素是head->next
双向链表:
①双向链表的遍历停止的条件是 p!=head
②双向链表的初始化head->next=head->pre=head
单向链表的逆转:使用p_pre、p、p_next三个指针
①p_pre=null;p=first_node;p_next=first_node->nest;
②p->nest=p_pre;p_pre=p;p=p_next;
③when p_next==null;p->nest=p_pre;break;
回调函数:
void funcA(int a, int b) //void ()(int, int)
{
printf("funcA...\n");
printf("a = %d, b = %d\n", a, b);
}
//定义一个 指针, 指向 void ()(int, int)
void(*fp)(int, int) = funcA; //fp --> funcA
(*fp)(a, b);//等价fp(a, b);//调用函数指针指向的函数
void funcD(int c, void(*fp)(int, int)) ;
//fp = funcA //让一个函数指针fp 指向funcA函数的执行的入口地址
{
int a = 100;
printf("funcD ...\n");
printf("c = %d\n", c);
//在funcD种 调用了另外一个函数 ,就说传进来的函数,就是funcD函数一个回调。
fp(a, c); //间接的调用了 funcA
//funcA(a, c);
}
//FP 是一种指针, 返回值是void 参数2个 分别是int,int 函数类型的 指针
typedef void(*FP)(int, int);
void business(int a, int b, FP fp)
{
//固定业务1
printf("a = %d\n", a);
//固定业务2
printf("b = %d\n", b);
//固定业务3
//....
//子业务 (可根据用户的不同的实现放可配置)
fp(a, b);
}
续行符:’\’
#define STR "hello, "\
"world\n"
预处理函数没有词法分析、语法校验,且有边际问题,即直接展开没有预编译审查:
#define FREE(p) \
do { \
free(p);\
p = NULL;\
} while(0)
在写宏函数的时候,使用do{}while(0)可以防止边际问题,将多个语句化为一个整体。
#define STR(s) #s //#s 就是将这个s的代码 都编程字符串
printf("%s\n", STR(main()));
#define A(first, second) first##second //这个拼接不是字符串的拼接,是token的拼接
A(printf, ("%s\n", "abc"););
#define showlist(...) \
printf(#__VA_ARGS__) // __VA_ARGS__ 就表示 ...接收的 所有参数
showlist(dsadjsadsjksaldsjadlsajsa,dsadsad,dsadasd,dsadsa,dsad);
用宏封装调试信息:
#define DEBUG(format, ...) \
fprintf(stderr, "[DEBUG][%s:%d][%s][%s]"format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);//__VA_ARGS__ === a,b
#define ERROR(format, ...)\
fprintf(stderr, "[ERROR][%s:%d][%s][%s]"format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);
#define LOG(format, ...)\
fprintf(stderr, "[LOG][%s:%d][%s][%s]"format, __FUNCTION__, __LINE__, __DATE__, __TIME__, ##__VA_ARGS__);
ERROR("malloc p error\n");
DEBUG("a= %d, b = %d\n", a, b);
LOG("a = %d, b = %d\n", a, b);
dll:最终的执行代码
lib:告诉编译器dll在哪
windows先找lib文件,通过lib文件知道dll在哪
注意:dll文件只能生成出,不能单独执行dll文件(编译文件后再执行文件才会生成dll文件)。由于lib、dll文件必须同时存在,VS编译器默认只生成dll文件,需要在每个动态库的函数定义、声明前都添加__declspec(dllexport),编译文件后再执行文件才会生成dll文件,此时会报错(忽略它),但是生成了lib文件。
给别人使用自己生成的第三方库的时候要包含三个文件:dll、lib、.h
检查内存泄漏:
linux:valgrind
windows:memwatch
windows检查需要的操作:
①将memwatch的.h、.c文件正确的加入到工程中
②将memwatch的.h文件加入到工程的主函数所得的文件中
③工程名右键属性->C/C++->预处理器->预处理器定义->下拉框选择编辑->添加两个宏(用两行):MW_STDIO、MEMWATCH