Chapter 2 指针
2.1 变量与内存地址
我们都很熟悉变量的声明赋值语句,例如:
int a=5;
在这个简单的语句中,编译器实际上做了如下大量的工作:
- 创建变量名 a
- 为变量分配存储空间.假设这段空间的地址是0xFF6600
- 在地址空间存入值"5"
当然,以上描述是抽象性的,不涉及实际的技术细节.
当我们使用这个变量,例如打印a的值时,编译器会做如下工作:
- 查找变量a
- 查找变量a的地址空间
- 从地址空间取值
在许多"现代"的高级编程语言里,声明变量,取值,一切都显得很自然,因为编译器隐藏了变量关于地址的细节.而C将地址细节提供给程序员,鼓励程序员写出更快效率更高的程序.
2.2 指针(Pointer)的概念
为了精确的理解指针,请区分变量值和变量的精确含义.变量a的值是字面量(literal text)5 ,这个"5"不能再改变. 但是变量a的值可以改变为6,7,8...等等.有时候你可以把变量理解为一个小仓库,里面的东西可以搬来搬去.
指针正是这样一个小仓库.不同的是char类型变量存储字符串的值,int类型存储数值型值,而地址类型(指针)存储地址的值.例如如下我们声明一个指针pa,并用&符号取出a地址并赋给pa:
int a=5;
int* pa=&a;
用一句简单的printf,你可以看到pa的值,即a的地址:
printf("Value of pa is:%x",pa);
当然,由于pa本身也是变量,根据2.1的描述,变量本身也有地址,我们可以试试打印pa的地址如下:
printf("Address of pa is:%x",&pa);
*用于指针前表示取值,即"取指针所存储地址所存储的值".例如a的值是5,a的地址是0x66ff00,pa的值是0x66ff00,则*pa表示0x66ff00上所存储的值,也就是5.所以*pa==5;为了避免这种拗口的叫法,通常*pa也称为"取pa所指向变量的值."
为了帮助你更好的理解指针,请确保自己理解如下几个概念:
- 地址:变量所分配到的存储空间.例如char型的存储空间是1字节,int型是4字节(在现代32位操作系统).
- 变量的值:变量所被分配存储空间所存放的字面量.例如字符型的'a','b','c',数值型的1,2,3,地址型的0xFFCC00,0xFFCCFF 等.
- 指针:地址的变量.指针的值不是普通的如1,2,3,'c','d','f' ... 等字面量,而是内存的地址.
- & :从变量中取出地址.
- * :取pa所指向变量的值.
国内一些不太精确的教科书经常将"指针"和"指针变量"概念混用,让人瞠目不知所云.根据如上的精确定义,"指针"应指地址变量,"指针变量"应指地址变量的变量.所以"指针"并不等于"指针变量".请观察如下例子:
int a=5;
int* pa=&a;
int* ppa=&pa;
上例中,a是变量,pa是指针,ppa可称为指针变量,或指针的指针.有趣的是,当你声明指针的指针时,观察如下例子:
int*************************************************** ppa=&pa;
在我的gcc3.45中编译运行正确.
这里的讨论并不仅仅为了咬文嚼字.回想过去学习指针的经历,许多国内教材的翻译水平让我抓狂.如果你实在厌烦了玩弄文字把戏,你可以到官方网上进行学习: www.cplusplus.com
最后,请运行这个例子以加深巩固本章节的学习(为方便对比,我将地址值以10进制形式输出):
#include <stdio.h>
#include <stdlib.h>
int main() {
int a=5;
int* pa=&a;
printf("Value of a is : %d\n",a);
printf("Address of a is : %d\n",&a);
printf("Value of pa is : %d\n",pa);
printf("Value of which pa pointed to is : %d\n",*pa);
printf("Address of pa itself is : %d\n",&pa);
int*************************************************** ppa=&pa;
printf("Value of ppa is : %d\n",ppa);
printf("Value of which ppa pointed to is : %d\n",*ppa);
printf("Address of ppa itself is : %d\n",&ppa);
//system("pause");
return 0;
}
2.3 指针和数组
数组和指针的关系极其紧密.数组由一系列类型相同的元素组成,这些元素的地址是连续的.事实上,数组名本身就是一个指针,只不过该指针的值不能再更改(称为常量指针).
以下这个例子会让你加深理解:
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[]= { 3, 7, 9, 11, 1, 6, 7, 5, 4, 2 };
printf("1.What's arr? %d\n", arr);
printf("2.What's &arr? %d\n", &arr);
printf("3.What's &arr[0]? %d\n\n", &arr[0]);
printf("4.What's arr[-2]? %d\n", arr[-2]);
printf("5.What's arr[2]? %d\n\n", arr[2]);
int* pa=&arr;
pa+=4;
printf("6.What's *pa? %d\n", *pa);
printf("7.What's pa[2]? %d\n", pa[2]);
printf("8.What's pa[-2]? %d\n", pa[-2]);
return 0;
}
有一个特性你必须知道,当数组作为函数的形参时,它实际上是一个指针.在紧接着数组声明后用sizeof函数,你可以得到数组的总地址空间,而在函数内,你用sizeof仅得到指针本身的大小(32位机器上是4字节). 例如:
#include <stdio.h>
#include <stdlib.h>
int strlen(char* a){
printf("Size of a is:%d\n",sizeof(a));
int c=0;
while(*a++)c++;
return c;
}
int main() {
char arr[]="Hello,world!";
printf("Size of arr is:%d\n",sizeof(arr));
//This couldn't be compiled.
//arr++;
int l=strlen(&arr);
printf("Length of array is:%d\n",l);
return 0;
}
2.4 指针和字符串
声明字符串可以用数组或指针方式.但这两种方式存在差异.例如:
char a[20]="Hello,world";
//a="hello";//This couldn't be compiled.
a[0] ='s'; //OK.
char* str="Hello,world";
str="Another world!";
str++;//OK.
//*str='s';//This couldn't be compiled.
//str[0]='s';//This couldn't be compiled.
总结数组方式和指针方式声明字符串的区别如下:
- 数组方式有实际的空间,所以可以单独改变元素值.而指针方式不能.
- 虽然都是指针变量.但指针方式的指针可以运算,而数组方式不能.
- 指针方式中的指针可以指向另一个字符串,而数组方式的指针不能.所以要改变一个数组的值,只能逐个元素进行改变.
以下这个例子演示了如何使用系统函数strcpy
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char* a="I'm a big enough buffer from string copied";
char b[80];
char* s="This is a beautiful world.";
char* t=NULL;
//t = strcpy(a,s);//Couldn't be run!
printf("t is : %s\n",t);
t =strcpy(b,s);
printf("t is : %s\n",t);
return 0;
}
技术上,指针形式定义的字符串变量实际上指向常量的字符串,该常量不能改变.有关常量和指针的关系,我们在下一节继续讨论.
2.5 指针和常量
常量是什么就不多加讨论了.见如下例子:
#include <stdio.h>
#include <stdlib.h>
int main() {
const int a=5;
printf("a is:%d\n",a);
//a=6;//This will not be compiled.
}
常量(的)指针(pointer to constant )即指向常量的指针.也就是说,假设*p==5,进行*p=6的操作会失败.
相反,指针(的)常量(constant pointer )表示指针的值不能改变,而指针指向的对象的值可以改变.
数组名本身就是一个指针常量,所以数组名不能进行通常的指针运算.而用指针声明字符串时,该指针是指针常量,所以不能再改变各元素的值.
如果你觉得这个中文意义的区分有点拗口,请牢记代码方式:
#include <stdio.h>
#include <stdlib.h>
int main() {
int a=5,b=7;
const int* c=&a;
//*c=8;//This couldn't be compiled.
c=&b;//OK.
int* const d=&a;
*d=10;
//d=&a; //This couldn't be compiled.
}
如果const在int*之前,则该变量是常量指针.const(常量)int*(指针)
如果const在int*之后,则该变量是指针常量.int*(指针)const(常量) .
是不是很好记忆,呵呵.
常量指针有着实际的实用意义.假设某个函数的形参为指针,在我们操作这个指针时,很容易把指针指向的对象值也改变,如果将该指针指向的对象声明为常量(即声明常量指针为形参),以下代码说明一切:
#include <stdio.h>
#include <stdlib.h>
int strlen(const int* str){
int c=0;
while(*str++){
//*s='t';//This couldn't be compiled.
c++;
}
return c;
}
int main() {
char s[]="Don't change this string!";
int l=strlen(s);
}
2.6 函数指针(Pointer to function)
乍一看,C语言并没有"接口"的概念,习惯使用Java的程序员可能对此有点失望.其实,回忆一下"回调函数"的概念(callback function).无论我们学哪种语言,都会被教导我们要将代码写在哪里,才能被编译器编译进而运行.最著名的回调函数就是main函数,我们将代码写在main里,编译器就会运行我们的代码(如果没有错误的话).
所以,回调函数其实就是接口.在C语言,这通过函数指针来实现.
先看看函数指针的语法:
int* someFunc1(int a);//一个普通的函数声明
int* (*someFunc2)(int a);//声明函数指针
必须通过观察代码才能更好理解函数指针的实际作用
#include <stdio.h>
#include <stdlib.h>
typedef char* (*YOUR_NAME)();
char* fun1(){
return "Diego";
}
char* fun2(){
return "Chen";
}
void handleName(YOUR_NAME y){
char* s=y();
printf("Welcome,%s\n",s);
}
int main() {
YOUR_NAME y1 = fun1;
handleName(y1);
handleName(fun2);
return 0;
}
这段代码的知识点有:
- 如何声明函数指针
- 如何为函数指针变量赋值.
- 实际的应用
粗略地看,以上例子似乎平平无奇,其实不然.稍具Java Servlet编程经验的程序员都知道,处理web请求是一个相当简单的事情----实现Servlet的doGet或者doPost函数则可.也许我们羡慕Servlet面向程序员的简易友好的接口并且想用C语言实现,于是我们考虑如下伪码:
typedef struct REQUEST_STRUCT
typedef void (*HANDLE_REQUEST_PROC)(REQUEST_STRUCT req)
void doGet(){
REQUEST_STRUCT req;
//Initializes req.
//Gets function from implementation by programmer.
HANDLE_REQUEST_PROC proc = ..//Gets from somewhere.
//Executes it.
proc();
//Continue other operations
} 如果没有函数指针的帮助,这简直不可能.
简单的说,函数指针的重要作用在于允许代码在运行时才进行连接.对于一些框架设计工作来说,预定义的供给程序员实现的回调函数是必不可少的,只有函数指针才能达到这个目的.
函数指针的语法总结如下:
- 如何声明(对比普通函数的声明).
- 如何赋值(仅需要函数名,不需要函数参数表)
void (* FUNCTIONS) (int a,int b);//声明函数指针
void func1(int a,int b);//符合该函数指针声明的函数
void func2(int a,int b);//符合该函数指针声明的函数
FUNCTIONS y = func1;//赋值
y=func2;//赋值
2.7 指针和动态内存分配
动态分配的内存地址空间是连续的,分配完的空间会返回起始地址的指针:
char* pstr = (char*)malloc(100*sizeof(char));
所以,在这个场合你还是会用到指针.关于动态内存的使用会有专题章节进行总结.
2.8 指针用法总结
C语言里关于指针的应用场合总结如下:
- 遍历数组元素
- 引用字符串
- 提供面向程序员的接口,技术上以函数指针实现.
- 分配动态内存