C语言指针和数组

谈到C语言编程,数组和指针是很多人的心头大石,总觉得它们是重点难点,重点是没错的,但绝不是什么难点,要说C语言的难点,客观地讲应该是带参宏,而数组和指针,概念浅显易懂,操作简洁方便,根本不是很多初学者想象的那么困难,所以一开始就要有充分的信心,其次,恰恰是因为它们的“方便”,导致如果一不小心会用错,所以数组和指针,尤其是指针,与其说它难,还不是说他容易用错,我们在使用的时候要格外小心。 

指针和数组,都涉及一个核心概念,就是地址,因此,我们从内存的地址开始给大家理清问题。 

内存是一个个的存储单元,每一个存储单元称之为一个字节(byte),一个字节有8位(即8bit)。我们存储数据的基本单位是字节,在32位的CPU架构下,最大能支持4G的内存,也就是1024 * 1024 * 1024 个字节,这些字节统统都要一个编号,用来方便访问它们,就像一幢大厦里面有很多房间,每个房间都有个门牌号,比如101,102,103,和201,202等等,不同房间的功能差别巨大,比如101是个会议室,102是个储物室,103可能是个厕所,功能千差万别,但是门牌号是一样的。 

完全一样的道理,内存单元的每一个字节都有编号,这个编号就是该字节的“地址”,比如0x00000001, 0x 0000FFFF, 这里之所以没有用101和102,只不过是因为内存地址太多了,三位数不足以表达,实际上我们需要一个32位的二进制数来表达,或者8位的十六进制数来表达。反正,每一个字节都有一个地址。 

好了,至此,我们明白了内存单元至少会有两个属性,一个是这个内存单元里面装的内容,比如一个整数,或者一个浮点数,或者一个字符,或者一个结构体,甚至是一段代码都可以,另一个属性是这块内存单元的地址,也就是门牌号。当这块内存单元包含很多字节的时候,我们拿最小的地址,也就是基地址作为整块内存的地址,也称为起始地址。 

比如 int a = 100, 这个变量 a 就是一块内存,内存里面放的内容是 100, 而这块内存的地址是 &a

再来 void f ( void ) { printf("helloworld"); } , 这个函数 f( ) 是一块内存,内存里面放的内容是一个打印 helloworld 的代码,而这块内存的地址是 &f 

明白了内存单元的地址这个概念之后,要理解数组和指针,就很简单了。首先来谈谈数组。 

在 C语言中我们是这样定义数组的: int a [10] ;

在上面的这个定义中,a 就是一个数组,是一个具有10个整型元素的数组,关键在于:这10个整型变量是一个挨着一个,紧密地排列在一起的,它们连成一片,我们将这整块内存起个名字,叫做 a。显然,由于每个整型变量的大小是 4 个字节,所以 整个数组的大小就是 4 * 10 = 40个字节。我们在来考虑 a 这个变量,这个变量的类型是 int [ 10] , 亦即 a 是一个具有10个整型元素的数组,那么它的值呢? 它的值就是 这块内存的基地址,也就是 第一个元素的地址。 

下面是重点,不管你以前是如何理解数组的,请抛弃你头脑中所有模棱两可的概念,重新站在编译器的角度(是的,编译器的角度,不是我的角度)理解数组的定义: 

当C编译器看到这样的定义语句:int a[10] 的时候,它会将这条语句拆分中两部分来看待,第一部分是 a[10] ,除此之外统统称为第二部分,在这里第二部分就是 int

第一部分: a[10 ] ,这里确凿无误地告诉编译器,请你给我一块连续的内存,而且这块内存要包含10个元素在里面。 说完之后你是不是觉得少了一点什么呢? 对了,你还没说这10个元素是什么呢?? 你要10个粽子还是要10根葱啊? 得说明白,这就是第二部分的事情了。

第二部分:int ,这里确凿无误地告诉编译器,刚才那10个元素,既不是粽子也不是小葱,而是10个整型变量。 ok,一切明白,我们要的就是一块连续的内存,里面装有10个整型变量,我们将这样的内存称为数组,准确地讲,这是一个具有10个元素的整型一维数组。 

问个问题,刚才我们的10个元素是 int ,那能不能是 float呢? 能不能是 char 呢? 能不能是结构体呢? 

答案是肯定的。下面再来从易到难再看两个例子:

int b[3][10]

有人看到以上定义可能会大叫:这是个二维数组! 是的,我们通常都会那么称呼它,但是现在咱们站在编译器的角度,编译器它可不认识什么二维数组,在它的法眼里,世界上只有一维数组,它实际上是这么看的: int (b[3]) [10]; 

第一部分:b[3] ,确凿无疑地告诉编译器,请你给我一块连续的内存,而且这块内存要包含3个元素在里面。这3个元素是什么呢?

第二部分:int [10] ,确凿无疑地告诉编译器,刚才那3个元素,既不是粽子也不是小葱,而是3个 int [10] 。 OK,一切明白,我们要的就是一块连续的内存,里面装有3个int [10] 变量,二 int [10] 是什么家伙呢? int [10] 就是上面说了半天的那个 int a[10], 准确地讲,这是一个包含了3个【具有10个整型变量的一维数组】的一维数组,这样说比较拗口,所以我们人为地发明了一个单词:二维数组。 

再来一个例子:

char *c[10];

因为方括号 [ ] 的优先级比星号高,因此这个定义语句要这么看: char * (c[10]) ; 编译器拿到这样的语句,毫无疑问地也会 分成两部分来分析:第一部分 c[10] ,因此这是一个具有10个元素的数组,那么这10个元素又是啥呢? 答案就是 第二部分: char * ,也就是说,这是一个存放了10个 char * 的数组,称之为 char 型指针数组,也就是专门用来存放 char * 的数组。 

好了,数组先到此打住,再来看指针的定义,你会发现编译器原来是有一套既定的统一的规则的。 

比如 int *p;

这个定义无比简单,就是定义了一个整型指针p,同样地不管你以前是怎么理解指针的,现在请你跟编译器站在一起,从它的角度来看看什么是指针,没错,我们又要将这个定义分成两部分了: 

第一部分: *p ,确凿无疑地告诉编译器,请你给我分配一块内存 p, 这块内存用来干嘛呢?因为p 的前面有个 星号,所以 p 既不是用来装猪饲料的,也不是用来装鸡蛋的,而是用来存放地址的! 前面已经说过,每一个字节都有一个编号,这个编号就是一个32位的二进制数,我们称之为该字节的地址,现在的这个 p,就是专门用来装地址的。既然是用来装地址的,那么要多大的变量才能装得下这个地址呢? 答案是在32位的系统里面需要4个字节,因为只有4个字节才能足以表达从 0b00000000 00000000 00000000 00000000 到 0b11111111 1111111 11111111 11111111 这样的内存单元地址。容易发现,每一个地址都是32位的一个二进制数,也就是需要4个字节来存放这个门牌号。 

第二部分:int , 上面第一部分已经确凿无疑地知道了p是一个用来装地址的变量了,关键是那个地址所对应的内存是什么呢? 这个问题有第二部分来回答,int,说明 p 将来存放的地址所对应的内存是一个 int,换句话讲,p 是一个专门用来存放 int 型量的地址的,我们亲切地将 p 称为 int 型指针。 

假如现在就有一个 int 型变量: int w = 100; 那么我们很自然地就可以将 w 的地址存放在 p 里面: p = &w ;这样,我们就说 p 指向了 w,如图:

现在明白了吧,所谓的指针,只不过就是用来装一个地址的内存而已,又因为我们可以将很多不同的量的地址交给指针来存储,所以又分为不同类型的指针,比如专门用来存放整型数据的地址的指针 int *p,我们把它称为整形指针,专门用来存放字符型数据的地址的指针 char *q ,我们把它称为字符指针,专门用来存放某一种函数的地址的指针

int (*k)( char ), 我们把它称为函数指针。

所有的指针都是用来存放地址的,而地址都是一个32位的形如 0b 00001101 00101101 00001110 11011101 这样的二进制数,(其实一般我们会用十六进制表示,比如0xFFFF1234),所以32位平台下的指针大小都是一样的,都是4字节的。

指针的区别不在于本身,而在于其所存放的地址所对应的数据不同,C语言中有各种各样的不同的数据类型,也就有各种各样不同的类型的指针,一般情况下不同类型的指针不能一起运算,或者那样的运算没有意义。指针的运算跟指针的类型是密切相关的。 

循序渐进,再来举一个例子:

int **q;

看到此定义语句,也一定会有人大呼:二级指针! 没错,民间惯称二级指针,但是站在编译器的角度看,事情就更简单了,我们沿用上面的规则,这条语句其实是这样的:

int * (*q);

第一部分,没错,是 *q ,所以,你可以很淡定地说,这个 q 跟上面的那个 p 没什么本质的区别,它们都是一个 4 字节的变量,都是专门用来存放别人的地址的。

关键是第二部分: int * ,这里说明,q 是一个专门用来存放 int * 型变量的地址的,刚好上面的那个 p 它就是 int * 的变量,于是我们就可以很自然地将 p 的地址赋值给q。

看到上面这幅图,p被称为一级指针,q 被称为二级指针,指针是一种可以间接访问的机制,比如现在要访问变量w,使得它的值变成200,可以有3种办法:

1, w = 200,

2. *p = 200,

3. **q = 200

以上三个式子都是等价的。 

下面讨论一下两个主题来结束本节内容,第一是函数指针问题,第二是函数中的数组参数问题。

函数指针,就是指向函数的指针,函数指针的用处非常广泛,在C程序开发中,函数指针的作用主要有两个,第一个是在结构体中增加函数指针,在C中实现面向对象,就像linux内核中的VFS子系统,是一个典型的面向对象思想的东西,但是都是用C语言写得,函数指针提供了操作数据的可能。第二个是用函数指针定义回调函数,来实现软件设计的分层。 

具体而言,函数指针的定义如下:

  1. int func(int i, char ch)
  2. {
  3. }
  4. int (*p)(int, char); // p就是一个专门指向形如func那样的函数的指针
  5. p = &func; // 使得p指向func
  6. p = func; // 取址符可省略
  7. func(100, 'a'); // 直接调用函数
  8. (*p)(100, 'a'); // 使用函数指针间接地调用函数
  9. p(100, 'a'); // 解引用符可省略

根据以上所述,函数指针有两种用法,第一是在结构体中实现面向对象,实际就是用函数指针指向操作结构体数据的函数,用LINUX中VFS子系统中的例子来说明问题:

  1. // VFS子系统的其中一个核心结构体,当系统打开一个文件时,内核用这个结构体来表达一个文件
  2. struct file {
  3. union {
  4. struct list_head fu_list;
  5. struct rcu_head fu_rcuhead;
  6. } f_u;
  7. struct path f_path;
  8. #define f_dentry f_path.dentry
  9. #define f_vfsmnt f_path.mnt
  10. const struct file_operations *f_op;
  11. spinlock_t f_lock;
  12. #ifdef CONFIG_SMP
  13. int f_sb_list_cpu;
  14. #endif
  15. atomic_long_t f_count;
  16. unsigned int f_flags;
  17. fmode_t f_mode;
  18. loff_t f_pos;
  19. struct fown_struct f_owner;
  20. const struct cred *f_cred;
  21. struct file_ra_state f_ra;
  22. u64 f_version;
  23. #ifdef CONFIG_SECURITY
  24. void *f_security;
  25. #endif
  26. void *private_data;
  27. #ifdef CONFIG_EPOLL
  28. struct list_head f_ep_links;
  29. struct list_head f_tfile_llink;
  30. #endif
  31. struct address_space *f_mapping;
  32. #ifdef CONFIG_DEBUG_WRITECOUNT
  33. unsigned long f_mnt_write_state;
  34. #endif
  35. };
  36. // 在上面的file结构体中,有个f_op的成员如下所示,里面都是函数指针,这些函数指针就是被用来操作表达文件的那些数据的。
  37. struct file_operations {
  38. struct module *owner;
  39. loff_t (*llseek) (struct file *, loff_t, int);
  40. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  41. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  42. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  43. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
  44. int (*readdir) (struct file *, void *, filldir_t);
  45. unsigned int (*poll) (struct file *, struct poll_table_struct *);
  46. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
  47. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
  48. int (*mmap) (struct file *, struct vm_area_struct *);
  49. int (*open) (struct inode *, struct file *);
  50. int (*flush) (struct file *, fl_owner_t id);
  51. int (*release) (struct inode *, struct file *);
  52. int (*fsync) (struct file *, loff_t, loff_t, int datasync);
  53. int (*aio_fsync) (struct kiocb *, int datasync);
  54. int (*fasync) (int, struct file *, int);
  55. int (*lock) (struct file *, int, struct file_lock *);
  56. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
  57. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
  58. int (*check_flags)(int);
  59. int (*flock) (struct file *, int, struct file_lock *);
  60. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
  61. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
  62. int (*setlease)(struct file *, long, struct file_lock **);
  63. long (*fallocate)(struct file *file, int mode, loff_t offset,
  64. loff_t len);
  65. };

函数指针的第二个用途,是用来实现软件的分层设计,这个比较抽象的概念,我们也用LINUX系统编程的一个实例来加以说明。 

我们知道LINUX系统提供信号机制,当某个进程收到一个信号的 时候,进程将会首先保存这个信号,当进程被调度时检查信号的阻塞掩码,然后检查该信号是否被用户层捕捉,然后再执行用户自定义例程或者执行默认动作,然后返回进程的被信号中断的代码继续执行,这个过程中用户定义的例程是LINUX内核没办法提供的功能,但是又是整个信号的响应过程的不可分割的一部分,因此内核就对用户层提供了一个接口,让用户自己来定义这个例程(即信号响应函数),然后在用户调用信号捕捉函数signal的时候顺便告诉内核用户自定义的信号响应函数,即传递一个函数指针来实现。 

下面是具体的代码:

  1. void func(int sig)
  2. {
  3. }
  4. ... ...
  5. signal(SIGINT, func); // 捕捉SIGINT,顺便告诉内核其响应函数是func,函数名func就是函数指针

这样,当进程收到SIGINT时,就会进入内核,执行内核的相关动作,然后内核按照用户层提供的func这个函数指针,回到用户控件调用func函数,因此这个函数也被称为回调函数,回调函数实现了不同的程序模块由不同的人开发,而且又可以协调合作的目的。

永不止步步 发表于02-10 10:24 浏览65535次
分享到:

已有0条评论

暂时还没有回复哟,快来抢沙发吧

添加一条新评论

只有登录用户才能评论,请先登录注册哦!

话题作者

永不止步步
金币:67417个|学分:363791个
立即注册
畅学电子网,带你进入电子开发学习世界
专业电子工程技术学习交流社区,加入畅学一起充电加油吧!

x

畅学电子网订阅号