话说学习和使用C语言也有几个年头了,可是对于它的指针着实是明白一阵糊涂一阵,今天把看到的一些指针的信息记录一下留个备案,不然以后又忘了。
一 以下几个指针变量的定义有什么区别:
const int *p; //(1)
int const *p; //(2)
int *const p; //(3)
const int *const p; //(4)
以前找工作的时候这题还真遇到过,不过工作找到了,然后就忘了。今天写代码遇到些问题,果断又糊涂了。网上说了一堆,还是自己写代码验证后才印象深刻。
以下代码哪些地方有错误?
#include <stdio.h> //1
int main() //2
{ //3
int a = 2; //4
int b = 3; //5
const int *p = &a; //6
int const *q = &a; //7
int *const r = &a; //8
const int *const s = &a; //9
p = &b; //10
*p = b; //11
q = &b; //12
*q = b; //13
r = &b; //14
*r = b; //15
s = &b; //16
*s = b; //17
return 0; //18
} //19
编译出现错误,分别在第11,13,14,16,17行。给出的错误分别是:
error: assignment of read-only variable
error: assignment of read-only variable
error: assignment of read-only location
error: assignment of read-only location
error: assignment of read-only variable
这表明(1)和(2)是没有区别的,它们表示定义了一个指针变量,这个变量指向的单元的内容不可修改,但是它的指向可以修改;(3)表示定义了一个指针变量,这个变量的指向不能修改,但是它指向的内容可以修改;(4)表示定义了一个指针变量,其指向和指向的内容都不能修改。
可以简单记忆:对于(1)和(2),只要把*p和*q替换成另外的变量如P和Q,那么就容易理解第11,13行为什么错误了(但是不严谨,因为P和Q不能反映指针的特性了,用它来替换定义时初始化赋值是解释不过去的)。这时有:
P = b;
Q = b;
因为const的变量除了定义时初始化外,不能再对其进行赋值操作,这样就很好懂了。后面两个不好理解,但是能把指针和指针所指内容分清楚就好理解了。今天在网上闲逛看见有人说指针变量定义时初始化如果这样写更好理解:
int a = 2;
int* p = &a; //一般习惯写成int *p = &a;
我觉得这对于理解指针的含义有很明显的帮助,但是对取指针指向的内容的理解就没有什么帮助了。
二 下面几段代码有何错误?
准备知识:
内存分配方式有三种:
从静态存储区域分配。 内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请指定大小的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活。
在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。分配的内存容量有限。
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
答案:程序崩溃。因为str指向的一直是NULL(这个可有(三)中的例子证明),在GetMemory()中的p是str的一个拷贝,执行p = (char *)malloc(100);后它的指向就已经变化了,而str还是指向原来的NULL。
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
答案:如准备知识中所讲,GetMemory()函数中的p指针变量是一个局部变量,它在GetMemory()执行完后就已经被释放了,如果侥幸它指向的内容没被其他的程序修改,还能打印出hello world。但是如果其他程序修改了p指向的内容,那么它可能打出来乱码。
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello world");
printf(str);
}
答案:能打印出hello world,但有可能造成内存泄漏。**p是一个指向指针的变量,假设p指向的中间指针变量为x,str指向的中间指针变量为&str那么最开始时有下图所示的情况:
p->x->NULL
^
str->&str->NULL
在执行了*p = (char *)malloc(num);之后,他们的指向情况变为:
p->x->y
^
str->&str->y
可以看出虽然&str的指向变化了,但是指向它的str的指向还是&str,是没有变化的。所以能打印出hello world。但是从上面也可以看到,由于每次调用GetMemory()都会重新分配内存,而原来分配的内存没有进行free或者delete操作,所以调用次数越多,已分配的内存会越来越多而未释放,所以可能造成内存泄露。
void main(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
答案:结果未知。因为str指向的内存内容已经被释放,str的指向不明(而不是NULL),只能人为释放。
三 为了验证(二)中的第一个问题,下面在Fedora 8上写了个简单的例子测试:
假设下面的程序中注释(1)打印的值为0xbfae9520,NULL打印的值为(nil),那么(1)~(4)打印的值分别是多少?
#include <stdio.h>
void try_to_modify_pointer(int *a);
int main()
{
int a = 7;
printf("before calling subroutine: addr of a: %p\n", &a); //(1)
try_to_modify_pointer(&a);
printf("after calling subroutine: addr of a: %p\n", &a); //(2)
return 0;
}
void try_to_modify_pointer(int *a)
{
printf("entering subroutine!\n");
if(a == NULL) {
printf("NULL poninter!\n");
} else {
printf("before assigning NULL to a: addr of a: %p\n", a); //(3)
a = NULL;
printf("after assigning NULL to a: addr of a: %p\n", a); //(4)
}
printf("leaving subroutine!\n");
}
打印分别是:
before calling subroutine: addr of a: 0xbfae9520
entering subroutine!
before assigning NULL to a: addr of a: 0xbfae9520
after calling subroutine: addr of a: (nil)
leaving subroutine!
after calling subroutine: addr of a: 0xbfae9520
这说明try_to_modify_pointer()中a最开始的指向与main函数中的a的地址是一样的,但是经过修改后它指向了NULL,try_to_modify_pointer()返回后我们看到main函数中的a的地址未发生变化。所以try_to_modify_pointer()中的a的指向只是main函数中a地址的拷贝,改变它的指向并不会引起main函数中a变量地址的变化。
四 函数指针
函数指针是指向函数的指针变量。在C语言中,数组名代表数组的首地址,同样函数名代表了函数的首地址,因此在赋值时,直接将函数指针指向函数名就行了。
它的定义格式为:
函数类型 (*指针变量名)(形参列表); //注意:函数类型 *指针变量名(形参列表);表示的是返回指针的函数
例如:对于函数int f(int a),我们定义一个指向该函数的函数指针fp,采用如下格式:
int (*fp)(int a);
对它赋值时,由于已知函数名就代表了函数的首地址,那么有:
fp = f;
赋值之后就是调用,例如调用时传递的参数是7,那么有:
(*fp)(7);
那么函数指针有什么作用呢?一个重要的用途就是实现函数回调。那么什么又是回调函数呢?下面按照我自己的理解来解释一下:
把软件分成系统(被调用者)和用户(调用者),用户(调用者)在系统上编写程序的时候会使用到系统(被调用者)的一些API,在软件层次上即上层接口调用下层接口,但是有时候系统(被调用者)是不知道用户(调用者)调用它来干什么事情,这就需要用户(调用者)写一个接口告诉系统(被调用者):我调用你的时候你按照我给你的方法做。那么这个由用户(调用者)实现来告诉系统(被调用者)怎么做的接口就叫做回调函数。这个过程在软件层次上是下层接口调用上层接口。而回调函数就可以通过函数指针来实现。
例如:系统有个按钮接口,我们开始要调用它来做一些基本的操作,但是按钮的响应是怎样的是我们需要做的。那么按钮接口中就应该有个函数指针,通过它我们就可以把我们实现的回调函数注册进去。简单起见,假设系统的按钮接口是button(),我们的回调函数是action(),那么有下面的例子:
#include <stdio.h>
//系统的button接口
void button(void (*func)(void))
{
if(func == NULL) {
printf("Default action!\n");
} else {
(*func)();
}
}
//用户的回调函数
void action(void)
{
printf("Hello world!\n");
}
int main()
{
void (*func)();
func = action; //注册回调函数
button(func); //调用系统的button接口
return 0;
}
其实从上面的例子可以看出,我们调用了系统的button接口,而系统又调用了我们的action接口,如下图所示:
|用户程序,action()|
| ^
v |
|系统程序,button()|
五 返回指针的函数
它的定义格式为:
函数类型 *函数名称(形参列表)
例如:char *func(void);
它可以用于解决(二)中的第一个问题,稍微修改下:
char *GetMemory(void)
{
char *p = (char *)malloc(100);
if(p == NULL) {
printf("malloc failed!\n");
return NULL;
}
return p;
}
main函数中调用方法也修改一下:
void Test(void)
{
char *str = NULL;
str = GetMemory();
if(str != NULL) {
strcpy(str, "hello world");
printf(str);
free(str); //为什么要添加这一句和下一句?
str = NULL;
}
}
可以这么修改是因为malloc()系统调用是在堆区由程序员分配内存的,GetMemory()函数返回后p指向的内容不会被释放。由程序员负责何时释放。但是如果修改成下面的样子就是错的(这个错误我曾经犯过,还是黄给我指出的):
char *GetMemory(void)
{
char p[100];
return p;
}
这是因为这时p是一个局部数组变量,它存在于栈区,调用GetMemory()之后p就已经被自动释放了。
六 数组指针和指针数组
简单的数组指针比较好理解,例如:
int *p;
char *p;
但是下面这种形式的就稍微复杂了:
int (*p)[4];
它是拿来做什么用的呢?一般见得不多,但是在二维数组中有用到的地方:
int a[3][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } };
p = a;
p++;
printf("*(*(p + 1) + 2) = %d\n", *(*(p + 1) + 2));
上面的打印是什么?
答案:打印是11。
int (*p)[4];表示定义了一个整形指针,且这个指针所指对象是以4个整形数为一个单元的,所以执行p++,它会步进4个整形数。定义这种指针必须要注意它与二维数组的列大小保持一致。
指针数组的定义:
int *a[4];
char *str[4];
如何初始化呢?如下所示:
int *b[4] = { &a[0][1], &a[1][2], &a[2][3], &a[1][1] };
char *str[4] = { "hello", "welcome", "OK", "fine" };
因为指针数组的每个元素表示是指针,所以必须使用地址,而字符型指针可以表示一个字符串,所以可以像上述那么初始化。一般字符型的指针数组用得比较多,正是因为字符型指针可以表示一个字符串的缘故。