yunshichen

我相信人生是值得活的,尽管人在一生中必须遭受痛苦,卑劣,残酷,不幸和死亡的折磨,我依然深信如此.但我认为人生不一定要有意义,只是对一些人而言,他们可以使人生有意义. ---J 赫胥黎

C语言速记 第二章 指针

Chapter 2 指针

2.1 变量与内存地址


    我们都很熟悉变量的声明赋值语句,例如:
    int a=5;
   
    在这个简单的语句中,编译器实际上做了如下大量的工作:
  1. 创建变量名 a
  2. 为变量分配存储空间.假设这段空间的地址是0xFF6600
  3. 在地址空间存入值"5"
    当然,以上描述是抽象性的,不涉及实际的技术细节.

    当我们使用这个变量,例如打印a的值时,编译器会做如下工作:
  1. 查找变量a
  2. 查找变量a的地址空间
  3. 从地址空间取值
    在许多"现代"的高级编程语言里,声明变量,取值,一切都显得很自然,因为编译器隐藏了变量关于地址的细节.而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语言里关于指针的应用场合总结如下:
  • 遍历数组元素
  • 引用字符串
  • 提供面向程序员的接口,技术上以函数指针实现.
  • 分配动态内存


posted on 2008-04-25 18:13 Chenyunshi 阅读(356) 评论(0)  编辑 收藏 引用 所属分类: C/C++

只有注册用户登录后才能发表评论。
<2008年4月>
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

导航

统计

常用链接

留言簿(7)

随笔分类

随笔档案

文章分类

相册

搜索

最新评论

阅读排行榜

评论排行榜