优秀的编程知识分享平台

网站首页 > 技术文章 正文

常指针、函数指针、结构体内部指针、通用指针原理解读

nanyue 2025-10-14 02:31:41 技术文章 2 ℃

若文章对您有帮助,欢迎关注 程序员小迷 。助您在编程路上越走越好!




[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 *类型变量的数据进行直接操作。






微风不燥,阳光正好,你就像风一样经过这里,愿你停留的片刻温暖舒心。

我是 程序员小迷 (致力于C、C++、C#、Android、iOS、Java、Kotlin、Objective-C、Swift、Shell、JavaScript、TypeScript、Python等编程技术的技巧经验分享),若作品对您有帮助,请关注、分享、点赞、收藏、在看、喜欢,您的支持是我们为您提供帮助的最大动力。

若文章对您有帮助,欢迎关注 程序员小迷 。助您在编程路上越走越好!

Tags:

最近发表
标签列表