1、关键字
c语言关键字有32个。sizeof是关键字不是函数。define不是关键字。
auto.int.short.double.float.char.if.else.switch.case.default.break.register.const.extern.sizeof.while.for.goto.
do.continue.return.void.enum.typedef.union.volatile.unsigned.struct.signed.long.static.
2、定义和声明 区别
定义:编译器创建一个对象,为这个对象分配一块内存并给它取个名字,该名字就是变量名或对象名。(一个变量或对象在一定的区域内比如函数内或全局,只能被定义一次,如果定义多次,编译器会提示重复定义)
声明:①告诉编译器,这个名字已经匹配到一块内存上了。声明可以多次出现。
②告诉编译器,这个名字我已经预先定了,别的地方再也不能用它来作为变量名或者对象名。
记住区别:定义创建了对象并为其分配了内存,声明没有分配内存。
3、最宽恒大量的关键字---auto
编译器默认缺醒情况下,所有变量都是auto的。
4、最快的关键字---register
register请求编译器尽可能的将变量存在CPU内部寄存器中而不是通过内存寻址访问以提高效率。是尽可能不是绝对。
使用register注意:
register变量必须是能被CPU寄存器所接受的类型。意味着register变量必须是一个单个的值,并且其长度应该小于或整型的长度。热切register变量可能不存放在内存中,所以不能用取值运算符&来获取register变量的地址。
5、static
作用一:修饰变量。变量又分为局部和全局变量,但他们都存在内存的静态区。
作用二:修饰函数。
5、皇帝身边的小太监---寄存器
CPU相当于我们的皇帝,大臣相当于内存,小太监就是我们的寄存器,数据从内存中拿出来先放到寄存器,然后CPU再从寄存器里读取数据来处理。处理后同样把数据通过寄存器存放到内存中。CPU不直接和内存打交道。
6、基本的数据类型:
short、int、long、char、float、double 6种基本数据类型(数值类型和字符类型)
7、最冤枉的关键字---sizeof
sizeof是关键字不是函数。
记住:sizeof在计算变量所占空间大小时,括号可以省略,而计算类型(模子)大小时不能省略括号。
8、sizeof(int) *p表示什么意思?
9、signed、unsigned关键字
计算机的底层只认识0、1.任何数据到了底层都会变计算转换为0、1.那么负数的‘-’号无法存入内存。好办,做个标记,把基本数据类型的最高位腾出来,用来存符号,同时约定,如果最高位为1,表明该数为负数,最高位为0,为整数。
一个32位的signed int类型的值范围-2的31次方~2的31次方-1;8位的signed int类型的值范围-2的7次方~2的7次方-1.
一个32位的unsigned int类型的值范围0-2的32次方-1。8位的unsigned int类型的值范围0~-2的8次方-1.
signed关键字也很宽恒大量,你也可以完全当它不存在,编译器缺省默认情况下数据位signed类型。
10、负数在计算机系统中,用补码来存储的。主要原因是使用补码,可以将符号位和其他位同一处理,同时减法也可以按加法来处理。另外,两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。
正数的补码与其原码一致;负数的补码:符号位为1,其余位为该数绝对值的原码按位取反,然后整个数加1.
11、if - else
#ifndef FALSE
#define FALSE 0
#endif
#ifndef TRUE
#define TRUE 1
#endif
11.1 bool变量与零值进行比较 (sizeof(bool) = 1)
bool bTtestFlag = FALSE;
if (!bTestFlag);
if语句是靠其后面的括号里的表达式来进行分支跳转的。
EPSINONde 的定义不能随便定义的。
#include <float.h>
就可以引用:FLT_EPSILON
DBL_EPSILON
LDBL_EPSILON
11.2 float和“零值”进行比较 (不能用浮点数直接做比较,浮点数都是有精度限制的)
float fTestVal = 0.0;(if (fTestVal == 0.0))直接比较是错误的!
if ((fTestVal >= -EPSINON)&&(fTestVal <= EPSINON)); EPSINON为定义好的精度。
分析:float和double都有精度限制的,不能直接拿来直接和0.0比。
EPSINON为定义好的精度。如果一个数落在[0.0-EPSINON,0.0+EPSINON]这个区间内,我们认为在某个精度内它的值与零值相等,否则不等。
11.3 指针变量和“零值”进行比较
int *p = NULL;//定义指针一定要同时初始化
if (NULL== p); if (NULL != p); //正确写法
12. else和那个if配对
C语言有这样的规定,else始终与同一括号内最近的为匹配的if语句结合。
13. if语句后面的分号
if-else语句中容出错的地方就是与空语句的连用。例:
if (NULL != P);
fun();
这样编译器把这个分号解析成一条空语句。即if (NULL != P); 等同于 if (NULL != p) { ;}
14. switch-case组合
if-else一般表示两个分支或是嵌套表示少量的分支,但如果分支很多的话,还是使用switch-case组合。
最后的default:break;也应该加上。
15. case关键字后面的值有什么要求吗?
case后面只能是整形或者字符型的常量或者常量表达式。
16. case语句的排列顺序
如果case语句少,也许可以忽略这点,但是如果case语句非常多,那就得好好考虑了!例如写的是某个驱动程序,也许会经常遇到几十个case语句的情况。一般来说遵循下面的规则:
①按照字母或数字顺序排列各条case语句。
②把正常情况放在前面,而把异常情况放在后面。
③按执行频率排列case语句。
17. do-while-for
C语言中循环语句有3种:while;do-while;for
while(condition)先判断while括号里的值,如果为真则执行其后面的代码。while(1)为死循环。
17.1 break和continue区别
break关键字表示终止本层循环。
continue表示终止本次(本轮)循环。当代码执行到continue时,本轮循环终止,进入下一轮循环。
do-while循环:先执行do后面的代码,然后再判断while后面括号里的值,如果为真,循环开始;;否则,循环不开始。
for循环,多用于事先知道循环次数的情况下。
17.2 循环语句的注意点
①建议在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,一减少CPU的跨切循环次数。
for (i = 0; i < 5; i++)
{
for (j = 0; j < 100; j++)
{}
}
②建议for语句的循环控制变量的取值采用“半开半闭区间”写法。
③循环尽可能的短,要使代码清晰
④把循环嵌套控制在3层以内
18. goto关键字
别用goto了!
19. void关键字
19.1 void空类型,void *则为空类型指针,可以指向任何类型的数据。void几乎只有注释和限制程序的作用,因为木有人会定义一个void变量。
void真正发挥作用在于:
①对函数返回的限定。
②对函数参数的限定。
19.2 void修饰函数返回值和参数
如果函数没有返回值,则声明为void类型。
19.3 void指针
①千万小心使用void指针类型。按照ANSI标准,不能对void指针进行算法操作。因为算法操作的指针必须是确定知道其指向数据类型大小的。也就是说知道内存目的地址的确切值。
②如果函数的参数可以是任意类型的指针 ,那么应该声明其参数为void *。
③void不能代表一个真实的变量
20. return关键字
return用来终止一个函数并返回其后面跟着的值。
①return语句不能返回指向”栈内存“的指针,因为该内存在函数体结束时被自动销毁。
21. const关键字
只读。其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容。(define不是关键字)
21.1 const修饰的只读变量
const int Max = 100;
21.2 节约空间,避免不必要的内存分配,同时提高效率。
编译器通常不为普通的const只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为了一个编译期间的值,没有了存储于读内存的操作,使得它的效率也很高。
例如:
#define M 3 //宏变量
const int N = 5;//此时并未将N放入内存中
。。。。
int i = N; //此时为N分配内存,以后不再分配
int I = M; //预编译器件进行宏替换,分配内存
int j = N; //没有内存分配
int J = M; //再次进行宏替换,又一次分配内存
21.3 修饰一般变量
int const i = 2; 或 const int i = 2;
修饰数组 int const a[5] = {1,2,3,4,5};
修饰指针:
const int *p;//p可变,p指向的对象不可变
int const *p;//p可变,p指向的对象不可变。
int *const p;//p不可变,p指向的对象可变
const int *const p;//指针p和p指向的对象都不可变
记忆方法:先忽略类型名,看const离哪个近。
21.4 修饰函数的参数
void fun(const int i);告诉编译器i在函数体中不能改变,从而防止了使用者的一些无意的错误的修改。
22. 最易变的关键字---volatile
volatile和const一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改。比如操作系统、硬件或者其它线程等。遇到了volatile声明的变量,编译器对访问该变量的代码就不在进行优化,从而可以提供对特殊地质的稳定访问。
例1:
int i = 10;
int j = i; (1)语句
int k = i; (2)语句
编译器对代码进行优化,因为在(1)(2)语句中,i没有被用作左值,这时编译器认为i的值没有发生改变,所以在(1)语句时从内存中取出i的值赋给j之后,这个值并没有被丢弃,而是在(2)语句中继续用来给k赋值。编译器不会生成会汇编代码重新从内存里取i的值,提高了效率。
例2:
volitale int i = 10;
int j = i; (1)语句
int k = i; (2)语句:
volatile关键字告诉编译器i是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值,因而编译器生成的汇编代码会重新从i的地址处读取数据赋值给k。这样看来,如果i是一个寄存器变量或者表示一个端口数据或多个线程的共享数据,就容易出错。所以volatile可以保证对特殊地址的稳定访问。
23. 最会带帽子的关键字---extern
extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中。
24. struct关键字
它将一些相关联的数据打包成一个整体,方便使用。在网络协议、通信控制、嵌入式系统、驱动开发等地方,我们经常要发送的不是简单的字节流(char型数组),而是多种数据组合起来的一个整体,其表现形式就是一个结构体。
24.1 空结构体多大?
结构体所占的内存大小是其成员所占内存之和。在visual c++6.0上测试结果不是0,是1.这是wsm呢,编译器认为你构造一个结构体数据类型是用来打包一些数据成员的,而最小的数据成员需要1个byte,编译器为每个结构体类型数据至少预留1个byte的空间。所以空结构体的大小定位为1byte。
25. 柔性数组flexible array
C99中,结构中的最后一个元素是未知大小的数组,这就叫做柔性数组成员,但柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包含柔性数组的内存。包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小。
26. struct与class的区别
在c++中,struct关键字与class关键字一般可以通用,只有个小差别。struct的成员默认情况下属性是public的,而class成员却是private的。
27. union关键字
union维护足够的空间来放置多个数据成员中的”一种“,而不是为每一个数据成员配置空间,在union中所有的数据成员共用一个空间,同一时间只能存储其中一个数据成员,所有的数据成员具有相同的起始地址。
一个union只分配一个足够大的 空间来容纳最大长度的数据成员。
27.1 大端小端模式对union类型的影响
大端模式:字数据的高字节存储在低地址,字数据的低字节则存放在高地址。
小端模式:字数据的高字节存储在高地址,字数据的低字节则存放在低地址。
27.2 如何用程序确认当前系统的存储模式?
思路:变量i占4个字节,但只有一个字节的值为1,另外3个字节的值都为0.如果取出低地址上的值为0,则为大端存储;为1则是小端存储。
int CheckSystem()
{
union check
{
int i;
char ch;
}c;
c.i = 1;
if (c.ch == 1)
printf("little\n");
else
printf("big\n");
return 0;
}
例:
int a[5] = {1, 2, 3, 4, 5};
int *ptr1 = (int*)(&a+1);
int *ptr2 = (int*)((int)a + 1);
&a代表数组的首地址 加1 表示指针移动的大小是整个数组的大小(偏移量为整个数组的大小),
(int)a将a转化为一个整数,如果原来a=0x100 那么(int)a+1=0x101(得到的值与平台的大小端存储有关)。
28. 枚举enum和#define的区别
①#define宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
②枚举可以一次定义大量的相关常量,而#define宏一次只能定义一个。
③sizeof一个枚举变量的大小是4
29. typedef关键字
typedef的真正意思是给一个已经存在的数据类型取一个别名,而非定义一个新的数据类型。
30. 逻辑运算符
|| 和 &&
||两边的条件只要有一个为真,其结果为真。
例:
int i = 0, j = 0;
if ((++i > 0) || (++j > 0))
{
printf("i = %d, j = %d\n", i, j);
}
结果是i = 1, j = 0;因为++i > 0为真,后面的++j 便不再计算。
30.1 按位异或 ^ (异或运算: 同为0,异为1)
a ^= b;
b ^= a;
a ^= b; 实现不用第3个临界变量交换两个变量的值。
30.2 左移、右移
左移和右移的位数不能大于数据的长度,不能小于0;
31. 内存对齐
字、双字、四字。
无论如何,为了提高程序的性能,数据结构尤其是栈,应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作2次内存访问,然而,对于对齐的内存访问仅需一次访问。
32. 指针和数组
int *p;
一个int*类型的模子在内存上咔出了4个字节的空间,然后把这个4个字节大小的空间命名为p,同时限定了这4个字节的空间里面只能存存储某个内存地址,,而且这个内存地址开始的连续4个字节上只能存储某个int类型数据。
int *p = NULL 和 *p = NULL;
32.1 数组
数组的内存布局:
int a[5]; a作为右值时,代表数组首元素的首地址(&a[0]),而非数组的首地址(&a)。
sizeof(a) = sizeof(int)*5;
sizeof(a[5]) = 4;因为sizeof是关键字,函数求值是在运行时,而关键字sizeof求值是在编译的时候,虽然并不存在a[5]这个元素,但是这里也并没有去真正访问a[5],而仅仅根据数组元素的类型来确定其值。
&a[0]和&a区别?
a[0]是一个元素,a是整个数组,虽然&a[0]和&a的值一样,但意义不一样。&a[0]是数组首元素的首地址,而&a是数组的首地址。
32.2 数组名a作为左值和右值的区别
简单而言,出现在赋值符”=“右边的就是右值,出现在赋值符”=“左边为左值。
例如:x = y;
左值:编译器认为x的含义是x所代表的地址。这个地址只有编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必考虑这个地址保存在哪里。
右值:编译器认为y的含义是y代表的地址里面的内容。这个内容是什么,只有在运行时才知道。
当a做为右值的时候代表是什么意思吗?其意义与&a[0]是一样的,代表的是数组首元素的首地址,而不是数组的首地址。但注意的是,仅仅是代表,并没有一个地方来存储这个地址,即编译器并没有为数组a分配一块内存来存其地址。
a不能作为左值!编译器会认为数组名作为左值代表的意思是a的首元素的首地址,但是这个地址开始的一块内存是一个总体,我们只能访问数组的某个元素而无法把数组当做一个总体来访问,所以我们可以把a[i]作为左值,而无法把a当作左值。
32.3 指针和数组之间的恩怨
它们之间木有关系。
指针就是指针,指针变量在32位系统下,永远是4个byte,其值为某一个内存的地址,指针可以指向任何地方,但是不是任何地方都能通过这个指针变量访问到。
数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存放任何类型的数据,但不能存函数。
例:
int a[5] = {1,2,3,4,5};
int *ptr = (int*)(&a+1);
printf("%d, %d\n", *(a+1), *(ptr-1)); 结果是 2, 5
分析:对指针进行加1操作,得到的是下一个元素的地址,而不是原有的地址直接+1. &a+1:取数组a的首地址,该地址加上sizeof(a)的值。即指向了下一个数组的首地址。将(&a+1)值强制转换为(int*)类型。
a和&a的值是一样的,a是数组首元素的首地址,也就是a[0]的地址&a[0];&a是数组的首地址。a+1是数组下一元素的首地址,即a[1]
33.4 指针和数组的定义和声明
①定义为数组,声明为指针
char a[100];
extern char *a;
分析:文件1中定义为数组,文件2中声明为指针,这样子搞是错误的。因为,定义分配内存,而声明没有,定义只能出现一次,而声明可以多次。这里的extern告诉编译器该名字a已经在别的文件中被定义了。对于左值和右值,如果编译器需要某个地址来执行某种操作的话,它就可以通过*来读或写这个地址的内存,并不需要先去找到存储这个地址的地方。
相反,对于指针而言,必须先找到存储这个地址的地方,取出这个地址值然后对这个地址进行开锁(取*)。
例如:
char a[] = “abcdefg”;在定义数组a的时候,编译器在某个地方保存了a的首元素的首地址(假设是0x000ff00),那么要取a[i]的值,则分为两步:①计算a[i]的地址,0x000ff00 + i*sizeof(char).②取出该地址中的值。
这也是为什么extern char a[]和extern char a[100]是等价的原因。因为声明不分配空间。所以编译器无需知道这个数组有多少个元素。这两个声明都告诉编译器a是别的文件中定义的一个数组,a同时代表着数组a的首元素的首地址,也就是这块内存的起始地址。数组内的任何元素的地址都只需要知道这个地址就可以计算出来。
但是当你声明为extern char *a;时,编译器理所当然的认为a是一个指针变量,在32位系统中占4个byte,在这4个byte中保存一个地址,这个地址存字符类型数据。虽然在文件1中,编译器知道a是一个数组,但是在文件2中 ,编译器并不知道,大多数的编译器是按照文件分别编译的,编译器只按照本文件中声明的类型处理,所以,虽然a实际大小是100个byte,但是在文件2中,编译器认为只占4个byte。
其次,我们知道,编译器会把指针变量中的任何数据当作地址来处理,所以,如果需要访问这些字符类型数据,我们必须先从指针变量a中取出其保存的地址。
②定义为指针,声明为数组
char *p = “abcdefg”; 文件1中
extern char p[]; 文件2中
分析:在文件1中,编译器分配了4个byte空间,并命名为p。同时p里保存了字符串常量“abcdefg”的首字符的首地址。这个字符串常量本身保存在内存的静态区,其内容不可改变。文件2中,编译器认为p是一个数组,其大小为4个byte,数组中保存的是char类型的数据。
指针p内保存的是字符串常量的首字符的首地址,编译器把指针变量p当作一个包含4个char类型数据的数组来使用,按char类型取出p[0],p[1],p[2],p[3],的值,但是并非我们所需的某块内存的地址。如果给p[i]赋值则会把原来p中保持的真正地址覆盖,导致再也无法找到其原来指向的内存。
34. 指针和数组的对比
通过上面的分析,相信你已经知道数组与指针的的确确是两码事了。它们之间不可混淆,但是我们可以“以xxxx的形式”访问数组的元素或指针指向的内容。
指针和数组的特性:
指针:①保存数据的地址,任何存入指针变量p的数据都会被当做地址来处理,p本身的地址由编译器另外存储。存储在哪我们并不知道。
②间接访问数据,首先取得指针变量p的内容把它作为地址,然后从这个地址中提取数据或向这个地址写入数据。指针可以以指针的形式访问*(p+i);也可以下标的形式访问p[i],但本质上都是先取p的内容然后在加上i*sizeof(类型)个byte作为数据的真正地址。
③通常用于动态数据结构
④相关函数malloc和free
数组:①保存数据,数组名代表数组首元素的首地址而不是数组的首地址。&a才是整个数组的地址。a本身的地址由编译器另外存储,存储在哪里,我们也不知道。
②直接访问数据,数组名a是整个数组的名字,数组内每个元素并没有名字。只能通过“具名+匿名”的方式来访问其某个元素,不能把数组当一个整体来进行读写操作。数组可以以指针的形式访问*(a+i),也可以以下标的形式访问a[i]。但本质都是a所代表的数组首元素的首地址加上i*sizeof(类型)个byte作为数据的真正地址。
③通常用于存储固定数目且数据类型相同的元素。
④隐式分配和删除
35. 指针数组和数组指针
35.1 指针数组和数组指针的内存布局
指针数组:首先它是一个数组,数组元素都是指针,数组占多少个字节由数组本身决定。简称:存储指针的数组。
数组指针:首先它是一个指针,它指向一个数组,在32位系统中永远占4个字节,至少它指向的数组占多少个字节不知道。简称:指向数组的指针。
例如:
int *p1[5];
int (*p2)[5];
分析:首先需明白一个符号之间的优先级问题。[]的优先级高于*,所以p1先与[]结合,构成了数组的定义,数组名为p1,int*修饰的是数组的内容,即数组的每个元素。因此,这是一个数组,其包含10个指向int类型数据的指针,即指针数组。
()的优先级高于[],所以*p2构成一个指针的定义,指针变量名为p2,int修饰数组的内容,数组在这里并没有名字,是个匿名数组,p2是指针,它指向一个包含10个int类型数据的数组,即数组指针。
35.2 在讨论a和&a之间的区别
例:
char a[5] = {'a','b','c','d'};
char (*p3)[5] = &a; //正确
char (*p4)[5] = a; //错误 两边数据类型不一致
分析:首先p3和p4都是数组指针,指向整个数组,&a是整个数组的首地址,a是数组首元素的首地址,其值相同意义不同。赋值号"="两边的数据类型必须是完全一致的。而p4这个定义的=号两边数据类型就不一致了。左边的类型是指向整个数组的指针,右边的类型是指向单个字符的指针。
36. 地址的强制转换
指针变量和整数相加减。(指针)
例:
int a[4] = {1,2,3,4};
int *ptr1 = (int*)(&a+1);
int *ptr2 = (int*)((int)a+1);
分析:ptr1将&a+1的值强制转换为int*类型,赋值给int*类型的变量ptr1,ptr1肯定指到了数组a的下一个int类型数据了。ptr1[-1]即
*(ptr1-1),即ptr1往后退4个byte,即为4;
ptr2: (int)a+1的值是元素a[0]的第二个字节的地址。然后把这个地址强制转换为int*类型的值赋给ptr2,即ptr2的值应该是a[0]的第二个字节开始的连续4个byte的内容。
37. 多维数组和多级指针
超过二维的数组和指针其实并不多用,如果能弄明白二维数组和二级指针,那么二维以上的也不是什么问题了。
37.1二维数组 int a[1][2]
我们平时可以把二维数组假想成一个excel表,实际上内存不是表状的,而是线性的。见过尺子吗,尺子和我们的内存非常相似,内存的最小单位是1个byte,内存是线性的,那么二维数组在内存里肯定也是线性存储的。
以数组下标的方式来访问其中的某个元素:a[i][j]。编译器总是将二维数组看成是一个一维数组,而一维数组的每一个元素又都是一个数组,a[n]这个一维数组的3个元素分别是a[0],a[1],a[2].
37.2 二维数组的初始化
int a[3][2] = {(0,1),(2,3),(4,5)};这里相当于int a[3][2] = {1,3,5};即a[0][0] = 1, a[0][1] = 3, a[1][0] = 5,其它元素都是0.
仔细发现花括号里嵌套的是小括号,而不是花括号!花括号里嵌套了逗号表达式,所以初始化二维数组一定要注意,别不小心把应该的花括号弄错成括号。
int a[3][2] = {{1},{3},{5}};
37.3 &p[4][2] - &a[4][2]的值是多少?
例:
int a[5][5];
int (*p)[5];
p = a;
&p[4][2] - &a[4][2] = ?
分析:当数组名a作为右值时,代表的是数组首元素的首地址。这里的a为二维数组,我们把数组a看多是包含5个类型元素的一位数组,里面再存了一个一维数组。如此,则a这里代表a[0]的首地址,a+1表示一维数组a的第二个元素,a[4]表示一维数组a的第5个元素,而这个元素中又存了一个一维数组,所以&a[4][2]表示的是&a[0][0] + 4*5*sizeof(int) + 2*sizeof(int).
p是指向一个包含5个元素的数组的指针,即p+1表示的是指针p向后移动了一个“包含5个int类型元素的数组”。这里1的单位是p指向的空间,即5*sizeof(int).
p[4]对于p[0]来说是向后移动了4个“包含4个int类型元素的数组”,即&p[4]表示为&p[0]+4*4*sizeof(int).由于p被初始化为&a[0],所以&p[4][2]表示的是&a[0][0] + 4*4*sizeof(int) + 2*sizeof(int).
解决这类问题,最好的办法就是画内存布局图。
38. 二级指针
二级指针是经常用到的。char **p;定义一个二级指针变量p,p是一个指针变量,在32位系统下占4个字节。它与一级指针不同的是,一级指针保存的是数据的地址,而二级指针保存的是一级指针的地址。
char ch;
char *p2;
char *p3;
p2 = &ch;
p3 = &p2;
39. 数组参数和指针参数
参数分为形参和实参数,形参是指声明或定义函数时的参数,而实参是在调用函数时主函数传递过来的实际值。
39.1 一维数组参数
能否传递一个数组?不能。无法向函数传递一个数组。
C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成为一个指向其首元素首地址的指针。这么做是有原因的,在c语言中,所有非数组形式的数据实参均以传值形式(对实参做一份拷贝并传递给被调用的函数,函数不能修改作为实参变量的值,而只能修改传递给它的那份拷贝)调用。然后如果拷贝数组的话,不论从空间还是时间上,其开销都是非常大的。更重要的是,在绝大多数情况下,你其实不需整个数组的拷贝,你只想告诉函数在那一刻对哪个特定的数组干兴趣。这样的话,为了节省时间和空间,提高程序的运行效率,就出现了上面的规则。
同样的,函数的返回值也不能是一个数组,而只能是指针。函数本身是没有类型的,只有函数的返回值才有类型。
39.2 一级指针参数
能否把指针变量本身传递给一个函数?
例:
void fun(char *p)
{
char c = p[1];
printf("%c\n", c);
}
int main()
{
char *b = "abcdefg";
fun(b);
return 0;
}
分析:b是main函数内的一个局部变量,它只在main函数内部有效(这里需要注意,main函数内的变量不是全局变量,而是局部变量,只不过它的生命周期和全局变量一样长而已)。全局变量一定是定义在函数外部的。既然b是局部变量,fun函数肯定无法使用b的真身,那么函数调用怎么办?好办,对实参做一份拷贝并传递给被调用的函数。传递到函数内的就是b的拷贝而非真正的b。
继续看例子:
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char));
}
int main()
{
char *str = NULL;
GetMemory(str, 10);
strcpy(str, "hello");
printf("%s moto\n", str);
free(str);
return 0;
}
分析:free并没有起到作用,内存泄露。运行strcpy(str, "hello");出现错误,这时观察str的值,发现仍然是NULL,即str本身并没有改变,malloc分配的地址并没有赋给str本身,而是赋给了str的拷贝,这这个拷贝是编译器自动分配和回收的,我们无法使用。
改良版:(二级指针)
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num*sizeof(char));
}
int main()
{
char *str = NULL;
GetMemory(&str, 10);
strcpy(str, "hello");
printf("%s moto\n", str);
free(str);
return 0;
}
分析:GetMemory(&str, 10);参数是&str,而不是str,这样的话传递过去的是str的地址,是一个值。在函数GetMemory内部,用*操作,*(&str)的值就是str。所以malloc分配的内存地址是真正赋值给str本身的。
方法3:使用return
char *GetMemory(char *p, int num)
{
p = (char *)malloc(num*sizeof(char));
return p;
}
int main()
{
char *str = NULL;
str = GetMemory(str, 10);
strcpy(str, "hello");
printf("%s moto\n", str);
free(str);
return 0;
}
40. 二维数组参数和二维指针参数
例:
void fun(char a[3][4]) == void fun(char (*p)[4])
分析:完全可以把a[3][4]理解为一个一维数组a[3],其中每个元素都是一个含有4个char类型数据的数组。根据规则:C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成为一个指向其首元素首地址的指针。改为数组指针char (*p)[4],编译器将p解析为一个指向包含4个char类型数据元素的数组,即一维数组a[3]的元素。
数组参数 等效的指针参数
数组的数组:char a[3][4] 数组的指针:char (*p)[4]
指针的数组:char *p[4] 指针的指针:char **p
注意:C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。这条规则不是递归的,即只有一维数组才是如此,当数组超过一维,将第一维改写成指向数组首元素首地址的指针之后,后面的维数不可在改变
例:a[3][4][5]作为参数时,可以被写为(*p)[4][5].
41. 函数指针
函数指针定义:顾名思义。函数指针就是函数的指针。它是一个指针,指向一个函数。
void (*fun)(char *1p, char *p2)
41.1 *(int*)&p ---是什么?
例:
void function()
{
printf("call function\n");
}
int main()
{
void (*p)();
//*(int*)&p = (int)function;
//p = function;
(*p)();
return 0;
}
分析:首先void (*p)();定义了一个指针变量p,p指向一个函数,该函数的参数和返回值都是void。
&p是求指针变量p本身的地址,这是一个32位系统的二进制常数。
(int*)&p表示将地址强制转换成指向int类型数据的指针。
(int)function表示将函数的入口地址强制转换成int类型的数据。
使用函数指针的好处在于,可以实现同一个功能的多个模块同一起来标识,这样一来更容易后期的维护,系统结构更加清晰。(便于分层设计,利于系统抽象、降低耦合度以及使接口与实现分开)
42. (*(void(*)())0)() ----?
首先: void(*)()是一个函数指针类型。该函数木有参数和返回值。
接着: void(*)()0,这是将0强制转换为函数指针类型,0是一个地址,也就是说一个函数存在首地址为0的一端区域内。
紧接着:(*void(*)()0),这是取0地址开始的一段内存里面的内容,其内容就是保存在首地址为0的一段区域内的函数。
最后: (*(void(*)())0)(),这是函数的调用。
43. 函数指针数组
char *(*pf)(char *p);很清楚,pf是一个函数指针,那么我们将其存储在一个数组中,char *(*ppf[3])(char *p);这就是定义了一个函数指针数组。它是一个数组,数组名为ppf,数组内存储了3个指向函数的指针,这些指针指向一些返回值类型为指向字符的指针、参数为一个指针字符的指针的函数。
43.1 函数指针数组的指针
函数指针数组指针不就是指针嘛!没那么复杂。只不过一个这个指针指向一个数组,这个数组里面存的都是指向函数的指针。
char *(*(*pf)[3])(char *p);
pf是指针,指向包含了3个元素的数组;这个数组里存的是指向函数的指针。
44.内存管理
44.1野指针
定义指针变量的同时最好初始化为NULL,用完后也将指针设置为NULL,避免野指针发送。
44.2 栈、堆和静态区
一般说来,我们可以简单的理解为内存分为3个部分,静态区、栈和堆。其实堆栈就是栈,而不是堆,堆heap,栈stack,栈也叫堆栈。
1、静态区:保存自动全局变量和static变量(包括static修饰的全局和局部变量)。静态区的内容在总个程序的生命周期都存在,由编译器在编译的时候分配。
2、栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数允许结束,这些内容会自动被销毁。其特点:效率高,但空间大小有限。
3、堆:由malloc系列函数或new操作符分配的内存。其生命周期由free或delete决定。在没有释放之前一直存在,直到程序结束。特点:使用灵活,空间比较大,但容易出错。
45. 常见的内存错误
45.1 指针没有志向一块合法的内存
1、结构体成员指针未初始化
struct student
{
char *name; //结构体成员指针未初始化
int score;
}stu, *pstu;
int main()
{
pstu = (struct student *)malloc(sizeof(struct student));
pstu->name = (char *)malloc(10); //如果结构体成员指针未初始化,而操作会出现内存错误
strcpy(pstu->name, "zrh");
pstu->score = 100;
printf("%s get score %d\n", pstu->name, pstu->score);
free(pstu);
return 0;
}
45.2 函数的入口校验
不管什么时候,使用指针之前一定要确保指针时有效的。
一般在函数的入口处使用assert(NULL != p)对参数进行校验。在非参数的地方使用if (NULL != p)来校验。
aeesrt是一个宏,而不是一个函数,包含在assert.h中,用来定位错误,而不是排除错误。
45.3 为指针分配的内存大小
为指针分配了内存,但是内存大小不够,导致出现越界错误。
char *p1 = "abc";
char *p2 = (char *)malloc(sizeof(char)*strlen(p1) + 1*sizeof(char));
因为字符串常量包含结束符'\0',所以容易分配大小不足。
45.4 内存泄露
造成泄露的内存就是堆上的内存。也就是说由malloc系列函数或new操作符分配的内存。
(void *)malloc(int size);malloc函数返回值是一个void类型的指针,参数是int类型数据,即申请分配内存大小,单位是byte。
46. 函数
递归的方式实现strlen函数。
int MyStrlen(const char *strDest)
{
assert(NULL != strDest); //#include <assert.h>
if ('\0' == *strDest) //确定参数传递过来的地址上的内存存储的是否是'\0',如果是则为空字符串,或者是字符串的结束标志
{
return 0;
}
else
{
return (1 + MyStrlen(++strDest)); //如果参数传递过来的地址上的内存存储的不是'\0',则说明这个地址上的内存上存储的是一个字符,既然这个地址上存储了一个字符,那么就计数为1,然后将地址加1个char类型元素的大小,然后再调用函数本身,如此循环,当地址加到字符串的结束标志符'\0'时,递归停止。
}
}
int my_strlen(const char *strDest)
{
assert(NULL != strDest);
return ('\0' != *strDest)?(1+my_strlen(strDest+1)):0;
}
如果 传入的字符串很长的话,就需要连续多次函数调用,而函数调用的开销比循环来说要大得多,所以,递归的效率很低,递归的深度太大甚至可能出现错误,比如栈溢出,所以,平时写代码,不到万不得已,尽量不要用递归。即便是用递归,也要注意递归的层次不要太深,防止出现栈溢出的错误,同时递归的停止条件一定要正确,否则,递归可能没完没了。
int fib(int num)
{
int ret;
if (num == 0 || num == 1)
{
return 1;
}
else
{
printf("%d\n", (num * fib(num - 1)));
return (num * fib(num - 1));
}
}
学习-C语言深度剖析 笔记!2013-02-20 17:51