深入理解C指针
个人认为,在没有任何计算机基础知识的情况下,就强行学习C/C++语言,是一件极为痛苦的事情。更重要的是,语言其实都是和计算机底层对应的。学习如何用只是让你知其然,在了解一些计算机基础知识之后,才能更好地知其所以然。指针就是最好的体现。没有一些内存的基础知识,使用指针往往容易出错。
指针是一个变量,其值为另一个变量的内存地址。使用指针的好处:
1.1 指针和内存
null指针
NULL宏
NULL宏是强制类型转换为void指针的整数常量0,在很多库中定义如下:
ASCII字符NUL
ASCII字符NUL定义为全0的字节 。
void指针
void指针是通用指针,可以用来存放任何数据类型的引用。任何指针都可以被赋值给void指针,他可以被转换为原来的指针类型 ,转换之后,指针的值和原指针的值 是相等的。void指针只能用作数据指针,不能用作函数指针。void指针具有与char指针相同的内存对齐方式。void指针和别的指针永远不会相等,不过,两个赋值为NULL的void指针是相等的。
1.2 指针的长度和类型
数据指针的长度通常是一样的,但是数据指针的长度和函数指针的长度是不一样的。
指针相关的预定义类型
1.3 指针操作符
1.3.1 指针的算术运算
给指针加上/减去整数
给指针加上/减去整数,实际上加/减的是这个整数和指针数据类型对应字节数的乘积。当指针与数组结合时,加上/减去整数需要注意的问题是数组越界问题。
两个指针相减
两个指针相减会得到两个地址之间的差值。这个差值通常用于判断数组中的元素顺序。指针之间的差值,是他们之间相差的单位数,ptrdiff_t类型用来表示两个指针差值的可移植方式。
比较指针
把指针和数组元素比较时,比较结果可以用来判断数组元素的相对顺序。
int vector[] = {28,41, 7};
int *p0 = vector;
int *p1 = vector + 1;
int *p2 = vector + 2;
printf(“p2 > p0: %d “, p2 > p0); // p2 > p0 : 1
printf(“p1 < p2: %d”, p1 < p2); // p1 < p2 : 0
void指针和加法
给void指针做加减法是标准C不允许的行为。
1.4 指针的常见用法
1.4.1 多层间接引用
多层间接引用也叫作双重指针。一个很好的例子就是使用argc和 argv 传递参数。
char *title[] = {
"hello world",
"hello java"
};
1.4.2常量与指针
指向常量的指针
指向常量指针,意味着不能通过指针来修改它所引用的值。
指向非常量的常量指针
指向非常量的常量指针,意味着指针不可变,但是,指针指向的数据是可以变换的。
2 . C的动态内存管理
指针的强大很大程度上源于他们能追踪动态分配的内存。C程序在运行时环境中执行,这通常是由操作系统提供的环境,支持栈和堆以及其他的程序行为。
2.1 动态内存分配
动态内存分配步骤:
如果不再使用已分配的内存却不将他释放,那么就会发生内存泄露,导致内存泄露的情况可能如下:
应该调动free函数却没有调用(有时称为隐式泄露)。
内存泄露的一个问题就是无法回收内存并重复利用,堆管理器可用的内存越来越少,在内存用光的极端情况下,操作系统可能崩溃。
丢失地址
int p = (int ) malloc (sizeod(int));
*p = 5;
// 再次对p分配地址,会导致丢失地址
p = (int *) malloc (sizeof(int)); //丢失地址
// 地址丢失的另一个例子
char name = (char)malloc(strlen(“Susan”) + 1);
strcpy(name, “Susan”);
while(*name != 0)
{
printf(“%c”, *name);
name++; //丢失地址
}
隐式内存泄露
如果程序应该释放内存然而实际上却没有释放,就会发生内存泄露。如果我们不再需要某个对象,但他任然在堆上,就会发生内存泄露。最常见的错误就是忘记free动态分配的内存。
2.2 动态内存分配函数
动态类型会根据指针的数据类型对齐。系统并不保证内存的连续性。动态内存分配函数有:
realloc
在之前分配的内存块的基础上,将内存重新分配为更大或者更小的部分。不改变原有内存块的数据。
请求的大小如果比当前分配的字节数小,那么多余的内存会还给堆,不能保证多余的内存会被清空;如果比当前分配的内存大,那么可能的话,就在紧挨着当前分配内存的区域分配新的内存,否则会在堆的其他区域分配并把旧的内存复制到新区域。
如果大小是0并且指针非空,那么就释放内存。如果无法分配空间,那么原来的内存块就保持不变,不过返回的指针是空指针,而且errno会设置为ENOMEN。
alloca
alloca函数在函数的栈帧上分配内存。函数返回后自动释放内存。但是这是个不标准的函数,应该尽量避免使用。
calloc
从堆上分配内存并清零。
// calloc 的典型用法
int *p = calloc (5, sizeof(int));
//上述表达式等价于下面这句
int *p = malloc(5 * sizeof(int));
memset(p, 0, 5 * sizeof(int));
2.3 用free释放内存
free函数应该指向由malloc类函数分配的内存地址,这块内存会被返还给堆。尽管指针任然指向这块区域,但是我们应该将它看成指向垃圾数据。
如果传递给free函数的参数是空指针,通常它什么也不做。如果传入的指针不是由malloc函数分配的,那么该函数的行为将是未定义的。
以下是几个使用free时会出现的问题:
将已释放的指针赋值为NULL
已释放的指针任然可能造成问题,如果我们试图解引用一个已释放的指针,其行为将是未定义的。避免该未定义行为的方法之一就是,将该指针显式的赋值为NULL,后续载使用这种指针时会造成运行时异常。使用这种技术的目的是为了解决迷途指针问题(迷途指针接下来解释)。
int p = (itn )malloc(5*sizeof(int));
…
free(p);
p = NULL;
重复释放
重复释放就是两次释放同一块内存。
int p = (itn )malloc(sizeof(int));
*p = 5;
free(p);
…
free(p); //二次释放
程序结束前释放内存
异常终止的程序可能无法做清理工作,而内存损坏可能正是应用程序终止的原因。是否应该在程序结束前释放内存取决于具体的应用程序。
2.4 迷途指针
如果内存已经释放,而指针还在引用原始内存,这样的指针称为迷途指针。迷途指针没有指向有效对象,有时候也称为过早释放。
迷途指针
使用迷途指针会引发一系列问题,包括:
导致这几类问题的情况可能如下:
迷途指针示例:
int *p = (int *)malloc(sizeof(int));
*p = 5;
free(p); // 指针已被释放
...
*p = 10; //迷途指针
执行free函数后将释放p所指向的内存,此后不该再使用这块内存了。但是大部分运行时系统都不会阻止后续的访问或修改。我们还是可以向这个位置写入数据,但是这么做的情况是不可预期的。
还有一种迷途指针的情况,这种情况很难被察觉:一个以上的指针指向同一块内存区域(内存别名),而其中一个指针被释放了。示例如下:
int *p = (int *)malloc(sizeof(int));
*p = 5;
int *p2 = p;
free(p2); //p2所指向的内存已经被释放,但是p还指向该处
*p = 10; // 迷途指针
处理迷途指针
可以使用以下方法对付迷途指针:
2.5 动态内存分配技术
C的垃圾回收是手动的,所谓的垃圾回收,是指将不再使用的内存收集起来已被后续使用,释放的内存称为垃圾,因此,垃圾回收指的就是这个过程。可以使用下面的技术更好的使用动态内存。
资源获取即初始化
资源获取即初始化(Resources Acquisition Is Initialization, RAII),用于解决C++中资源的分配和释放。即使有 异常发生,这种技术也能保证资源的初始化和后续的释放,分配的资源最终会得到释放。
使用异常处理函数
在finally块中放入free函数不管有没有发生异常,都会执行finally块,因此一定会执行free函数。
3.1 指针的堆和栈
要理解函数及其和指针的结合使用,需要理解程序栈,大部分现在的跨结构语言,比如C,都用到了程序栈来支持函数执行。调用函数时,创建函数的栈帧并将其推到程序栈上。函数返回时,其栈帧从程序栈上弹出。
栈帧由以下几种元素组成:
3.2 通过指针传递和返回数据
void test_func1(int *a, int *b) // 通过指针传递数据
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int *sum(int a, int b)
{
static int *s;
int sum = a + b;
s = ∑
return s;
}
从函数返回指针时需要注意的几个问题:
3.3 函数指针
int *sum(int a, int b) //函数
{
static int *s;
int sum = a + b;
s = ∑
return s;
}
// 声明一个函数指针
int* ((*ptr)(int a, int b));
/*
* 函数指针解读:
* ptr是一个指针,指向一个函数,函数的参数是a和b,函数的返回值是指向int的指针
*/
// 使用函数指针
ptr = sum;
/*
或者
ptr = ∑
*/
int a =3;
int b = 4;
int sum;
sum = *(ptr(&a, &b));
C中有两种类型的字符串。
wchar_t用来表示宽字符,要么是16位宽,要么是32位宽。这两种字符串都以NUL结尾,在string.h找到单字节字符串函数,在wchar.h找到宽字符串函数。创建宽字符串主要用于支持非拉丁字符。
4.1 标准字符串操作
声明字符串的方式有三种:字面量、字符数组和字符指针。
// 初始化方式1
char header[] = "Media Player";
/*
上式等价于:
char header[13];
strcpy(header, "Media Player");
*/
// 初始化方式2
char *header = (char *)malloc(strlen("Media Player")+1);
strcpy(header, "Media Player");
定义字面量时通常会将其分配在字面量池中,这个内存区域保存了组成字符串的字符序列。多次用到同一个字面量时,字面量池中通常只有一份副本。大部分编译器都有关闭字面量池的选项,一旦关闭,字面量可能生成多个副本,每个副本拥有自己的地址。
通常认为字符串是不可变的。但是这取决于具体的编译器,比如在GCC中,字符串字面值就是可以修改的。
字符串之间的比较使用strcmp函数,函数返回值如下
比较两个字符串有几种不正确的写法,第一种试图用赋值操作符作比较:
char command[16];
scanf("%s", command);
if (command = "Quit") // 不正确写法
这不是作比较,而且会导致类型不兼容的错误,我们不能把字符串字面量地址复制给数组名字。另一种写法是相等操作符:
char command[16];
scanf("%s", command);
if (command == "Quit") // 不正确写法
这样会得到假,因为我们比较的是command的地址和字符串字面量的地址。在本例中,我们试图把字符串字面量的地址赋给 command。command是数组,不用数组下标就把一个值赋值给这个变量是不可能的。
4.2 传递字符串
最常用的传递字符串的方式就是给应用程序传递参数:
int main(int argc, char *argv[])
4.3 返回字符串
函数返回字符串时,实际返回的是字符串的地址。要返回合法的地址,可以返回一下三种对象之一的引用:
像下面这样返回字面量的地址:
char *returnChar(int code)
{
switch(code)
{
case 100:
return "C";
case 101:
return "C++";
case 102:
return "Java";
}
}
以上这段代码会工作得很好。另一种方式就是返回静态字面量:
char *returnChar(int code)
{
static char* p1 = "C";
static char* p2 = "C++";
static char* p2 = "Java";
switch(code)
{
case 100:
return p1;
case 101:
return p2;
case 102:
return p3;
}
}
也可以在堆上分配字符串的内存然后返回其地址,但是要记得显式释放动态分配的内存。
安全问题和指针误用