`
java-mans
  • 浏览: 11415502 次
文章分类
社区版块
存档分类
最新评论

指针,温柔又危险----小话c语言(7)

 
阅读更多

[Mac-10.7.1 Lion Intel-based, gcc 4.2.1]


Q:指针到底是什么?

A:有一天,你初到合肥,要到一个地方,但是路线不熟悉。遂问了一个路人,请问乌邦托在哪里?路人答曰:向南走即可。这个回答就像指针一样,告诉你一个方向。但是,到底向南多少,这就像是指针类型决定的大小。


Q:看到那个const修饰指针的时候,老是搞不清楚到底是指针是常量还是指针指向的变量是常量?

A:其实很简单,采用从右向左的读法即可搞定这些。如下例子:

#include <stdio.h>

int main (int argc, const char * argv[])
{
    int i = 100;
    const int *pi = &i;
    *pi = 200;
    
    return 0;
}
保存为const.c,使用gcc -o const const.c编译:


可以看到编译错误,表明pi是个不可更改其指向数据的指针。按照上面的从右读的原则即为:

const int * pi

常量 整形 指向 pi

读为: pi指向整形常量,即pi指向什么变量可以改变,但是指向的变量的值不可改变。

当然,使用类型方法,const int *pi和 int const *pi的含义是一致的。


如下代码就是ok的:

#include <stdio.h>

int main()
{
    int i = 100, j = 200;
    const int *pi = &i;
    pi = &j;
    return 0;
}
编译ok.


另外一种情况:

#include <stdio.h>

int main()
{
    int i = 100, j = 200;
    int *const pi = &i;
    pi = &j;
    return 0;
}

编译:

可以看到,编译错误表示pi是只读的,不可以更改pi的值。再使用从右向左的读法:

int * const pi

整形 指向 常 pi

读为: pi常指向整形

这也意味着,pi的值不能更改,但是没有意味着pi指向的数据不可以更改。

#include <stdio.h>

int main()
{
    int i = 100, j = 200;
    int *const pi = &i;
    *pi = j;
    return 0;
}

如上代码,编译ok.


Q:看过很多代码中含有函数指针,它的本质是什么?

A:它的本质即为一个指针,理论上函数的地址在编译期即可计算得到(当然在链接或者运行时还可能有重新定位)。先看一个简单的例子:

#include <stdio.h>
#define PRINT_D(intValue)   printf(#intValue" is %d\n", (intValue));

int add(int a, int b)
{
    return a + b;
}

int main()
{
    int (*func)(int, int) = add;
    PRINT_D(func(1, 2))
    return 0;
}

保存为func_ptr.c, 编译运行:

分析下代码:

int (*func)(int, int)表示声明一个函数指针,它的参数是2个整形,返回值为1个整形;什么地方能体现出是函数指针呢?就在于func前面的*号。

= add; 表示此指针指向add函数。c语言的编译原则是函数编译之后可以确定当前编译状态的地址。为了确定,我们查看下汇编代码:

gcc -S func_ptr.c得到汇编(部分):

_add:
Leh_func_begin1:
	pushq	%rbp
Ltmp0:
	movq	%rsp, %rbp
Ltmp1:
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	movl	-4(%rbp), %eax
	movl	-8(%rbp), %ecx
	addl	%ecx, %eax
	movl	%eax, -16(%rbp)
	movl	-16(%rbp), %eax
	movl	%eax, -12(%rbp)
	movl	-12(%rbp), %eax
	popq	%rbp
	ret
Leh_func_end1:

	.globl	_main
	.align	4, 0x90
_main:
Leh_func_begin2:
	pushq	%rbp
Ltmp2:
	movq	%rsp, %rbp
Ltmp3:
	subq	$16, %rsp
Ltmp4:
	leaq	_add(%rip), %rax
	movq	%rax, -16(%rbp)
	movq	-16(%rbp), %rax
	movl	$1, %ecx
	movl	$2, %edx
	movl	%ecx, %edi
	movl	%edx, %esi
	callq	*%rax

可以看到_add在汇编代码中是一个标号,标号就意味着是一个地址。callq *%rax可以看到,这是调用add函数。


Q:用add赋值给func的时候,为什么不用&add,不是要取函数的地址么?

A:是的,这样也可以,不过函数本身就可以看成地址,它们效果一样的。

#include <stdio.h>
#define PRINT_D(intValue)   printf(#intValue" is %d\n", (intValue));
#define PRINT_P(ptr)        printf("%10s is %p\n", (#ptr), (ptr));
typedef int (*func)(int, int);

int add(int a, int b)
{
    return a + b;
}

int main()
{
    PRINT_P(add)
    PRINT_P(&add)
    return 0;
}

运行:


Q: int (*func)(int, int)这种写法有点复杂吧。

A:是的,避免写很多这种代码,可以使用typedef来定义一个函数指针。

#include <stdio.h>
#define PRINT_D(intValue)   printf(#intValue" is %d\n", (intValue));
typedef int (*func)(int, int);

int add(int a, int b)
{
    return a + b;
}

int main()
{
    func add_func = add;
    PRINT_D(add_func(1, 2))
    return 0;
}

Q:结构体里面不也是可以含有函数指针的么?这里的函数指针的作用是什么呢?

A:是的,可以含有。在结构体的函数指针一般为结构体数据服务的,它的实现可以模拟面向对象语言的类的功能。实际上,正因为有了指针,整个编程世界才变得很精彩,很多模拟方式都是通过指针达到的。

#include <stdio.h>
#include <string.h>
#define PRINT_D(intValue)   printf(#intValue" is %d\n", (intValue));
#define PRINT_S(str)        printf(#str" is %s\n", (str));

typedef struct Student
{
    int     age;
    char    name[32];
    int     (*get_age)(struct Student *s);
    void    (*set_age)(struct Student *s, int new_age);
    char    *(*get_name)(struct Student *s);
    void    (*set_name)(struct Student *s, char *new_name);
}Student;

int student_get_age(struct Student *s)
{
    return s->age;
}
void    student_set_age(struct Student *s, int new_age)
{
    s->age = new_age;
}

char    *student_get_name(struct Student *s)
{
    return s->name;
}

void    student_set_name(struct Student *s, char *new_name)
{
    memset(s->name, 0, sizeof(s->name));
    strncpy(s->name, new_name, sizeof(s->name));
}

int main()
{
    Student s;
    s.get_age = student_get_age;
    s.set_age = student_set_age;
    s.get_name = student_get_name;
    s.set_name = student_set_name;
    student_set_age(&s, 25);
    student_set_name(&s, "xichen");
    PRINT_D(student_get_age(&s))
    PRINT_S(student_get_name(&s))
    return 0;

编译运行:


Q:好像这个数组和指针的联系还是挺多的,下面的代码为什么输出的值不对?

#include <stdio.h>
#include <string.h>
#define	PRINT_D(intValue)	printf(#intValue" is %d\n", (intValue));

void    print_arr_size(int arr[3])
{
    PRINT_D(sizeof(arr))
}

int main()
{
    int arr[3] = {1, 2, 3};
    print_arr_size(arr);
    return 0;
}

运行结果:


A:这是因为数组形式作为参数会被退化成指针导致的,也就是说print_arr_size函数的参数int arr[3]其实等同于int *arr,所以sizeof(arr)的值是指针的大小(笔者的平台指针大小为8)。


Q:常常看到函数的原型中有void *,它到底代表什么?

A:比如,

void	*malloc(size_t);
申请空间,返回对应的指针;但是到底是什么类型的指针,此函数不能确定,是需要外部调用者来确定;所以,void *可以看成通用型指针,其它类型的指针均可以赋值给void *类型指针;而且,void *类型指针都可以通过强制转换变成需要的指针;用面向对象的思想来解释,void *是基类, char *, int *等都是子类。

char *p = (char *)malloc(32);
其它类型指针转换成void *的例子:

#include <stdio.h>
#define	PRINT_D(intValue)	printf(#intValue" is %d\n", (intValue));

int main()
{
    int i = 1;
    void    *p = &i;
    PRINT_D(*(int *)p)
    return 0;
}

当然,void *只是表示一种通用指针,到底此指针是什么类型的不确定,所以不能直接对void *类型变量的数据进行直接操作。



分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics