有抱负的程序员必须面对三重障碍,即掌握遍布程序设计语言中的各类术语、理解如何使用语言元素(而不仅仅只是知道它们的概念)以及领会如何在实际场景中应用该语言。
定义
可以存储地址的变量是指针(pointers),存储在指针的地址通常是一个变量。如图7-1所示。指针pnumber含有另一个变量number的地址,变量number是一个值为99的整数变量。存储在pnumber中的地址是number第一个字节的地址。"指针"这个词也用于表示一个地址。图7-1:
另外,知道变量pnumber是一个指针还不够,更重要的是,编译器必须知道它所指的变量类型。没有这个信息,根本不可能知道它占用多少内存,或者如何处理它所指的内存的内容。char类型值的指针指向占有一个字节的值,而long类型值的指针通常指向占有4个字节的值(32位系统)。因此,每个指针都和某个变量类型相关联,也只能用于指向该类型的变量。所以如果指针的类型是int,就只能指向int类型的变量,如果指针的类型是float,就只能指向float类型的变量。一般给定类型的指针写成type*,其中type是任意给定的类型。
类型名为void表示没有指定类型,所以void*类型的指针可以包含任意类型的数据项地址。
类型void*常常用做参数类型,或常常用做以独立于类型的方式处理数据的函数的 返回值类型。任意类型的指针都可以传送为void*类型的值,在使用它时,再将其转为合适的类型。例如,int类型变量的地址可以存储在void*类型的指针变量中。要访问存储在void*指针所指地址中的数值,必须先把指针转为int*类型。malloc()库函数分配在程序中使用的内存,返回void*类型的指针。
第一次遇到指针时,很可能会弄不清楚。指针有很多层意义,这就是混乱的根源。我们可以使用地址、数值、指针和变量,有时很难搞清楚到底是怎么回事。最好编写短一点的程序,使用指针得到数值,改变值,打印地址等。这是能有信心用好指针的唯一方法。
结合整篇文章去思考,或许会有那么点理解,它与内存的关系,它与数据类型的关系,内存与数据的关系等。
几种方法访问二维数组的元素
指向常量的指针
声明指针时,可以使用const关键字指定,该指针指向的值不能被改变。下面时声明const指针的例子:
long value = 99999L;
const long *pvalue = &value; //Defines a pointer to a constant
把pvalue指向的值声明为const,所以编译器会检查是否语句试图修改pvalue指向的值,并将这些语句标记为错误。例如,下面的语句就会让编译器生成一条错误的信息:
*pvalue = 8888L; //Error - attempt to change const location
pvalue指向的值不能改变,但不能使用pvalue指针做这个改变。当然,指针本身不是常量,所以仍然可以改变它指向的值。
long number = 8888L;
pvalue = &number; //OK - changing the address in pvalue
这会改变指向number的pvalue中的地址,仍然不能使用指针改变它指向的值。可以改变指针中存储的地址,但不允许使用指针改变它指向。
make a conclusion
这种形式"const long *pvalue"声明的指针,指针的值(也就是地址)可以被改变,但不能通过它去修改地址对应的值。
从对应英文“Defines a pointer to a constant”能看出,这个指针只是指向常量,value自己又能修改。如:
long value = 99999L;
const long *pvalue = &value; //Defines a pointer to a constant
value = 100000L;
printf("value = %d\n", value); //100000
printf("*pvalue = %d\n", *pvalue); //100000
常量指针
使指针中存储的地址不能改变。此时,在指针声明中使用const关键字的方式略有区别。下面的语句可以使指针总是指向相同的对象:
int count = 43;
int *const pcount = &count; //Defines a constant pointer
第二条语句声明并初始化了pcount,指定该指针存储的地址不能改变。编译器会检查代码是否无意把指针指向其他地方,所以下面的语句会在编译时生成一条错误信息:
int item = 34;
pcount = &item; //Error - attempt to change a constant pointer
但使用pcount,仍可以改变pcount指向的值:
*pcount = 345; //OK - changes the value of count
这条语句通过指针引用了存储在count中的值,并将其改为345。还可以直接使用count改变这个值。
make a conclusion
这个概念与“指向常量的指针”的概念可以说是相反的,让我想起我高中数学老师匡老师教的一个方法来记住这些相反意义的概念,这方法就是:就只记住第一个,遇到第二个时,记住跟第一个概念相反意思的就ok了
这种形式“int *const pcount”声明的指针,地址不可以改变,但地址对应的值可以被修改。
极端例子
可以创建一个常量指针,它指向一个常量值:
int item = 25;
const int *const pitem = &item;
pitem是一个所有信息都是固定不变的(除item自己)。它是一个指向常量的常量指针,不能改变存储在pitem中地址,也不能使用pitem改变它指向的内容。但仍可以直接修改item的值,如果希望所有信息都固定不变,可以把item指定为const。
内存的使用
C语言的一个功能:动态内存分配(Dynamic memory allocation)
动态内存分配,它是依赖指针的概念,它允许在执行程序时动态分配内存。只有使用指针,才能动态分配内存。
在程序的执行期间分配内存时,内存区域中的空间称为堆(head)。还有另一个内存区域,称为堆栈(stack),其中的空间是分配给函数的参数和本地变量。 在执行完该函数后,存储参数和本地变量的内存空间就会释放。堆中的内存是由程序员控制的。在分配堆上的内存时,由程序员跟踪所分配的内存何时不再需要,并释放这些空间,以便以后重用它们。
这两个内存概念,跟JVM的内存模型很像。不会所有编程语言都类似吧?
动态内存分配: malloc()函数
使用这个函数时,需要在程序中包含头文件<stdlib.h>。使用malloc()函数需指定要分配的内存字节数作为参数(但在实际还没使用这些内存,从操作系统层面是看不出占用内存的(这个是我自己实践,未有文献证明))。这个函数返回所分配内存的第一个字节的地址。根据之前说的指针概念,所以这里就必须使用指针。
动态内存分配的一个例子如下:
int *pNumber = (int*)malloc(100);
释放动态分配的内存
在动态分配内存时,应该总是在不需要该内存时释放它们。堆上分配的内存会在程序结束时自动释放,但最好在使用完这些内存后立即释放,甚至是在退出程序之前,也应立即释放。
在比较复杂的情况下,很容易出现内存泄露,此时无法释放内存。这常常发现在循环内部,由于没有释放不再需要的内存,程序会在每次循环迭代时使用越来越多的内存,最终占用所有内存。
要释放动态分配内存,而该内存的地址存储在pNumber指针中,可以使用下面语句:
free(pNumber);
pNumber = NULL;
free()函数的形参是void*类型,所以指针类型都可以自动转换为这个类型,所以可以把任意类型的指针作为参数传送给这个函数。只要pNumber包含你分配内存时返回的地址,就会释放所分配的整个内存块,以备以后使用。在指针指向的内存释放后,应总是把指针设为NULL。这个Java思维不太一样,以便利及考虑程序员的“善忘”,应该把“指针设为NULL”的步骤整合在free()函数里面才对的。(这里说的程序员“善忘”,不是真的善忘,而是容易被别的事情打断,从而忘记释放。这里就不说aop,代理的概念了,C好像没有这些概念) 。
用calloc()函数分配内存
这个函数好像只能在Windows下使用,作用也跟malloc一样,只是做了些安全性优化。为了不干扰指针的学习,这就略过。
扩展动态分配的内存
realloc()函数可以重用或扩展以前用malloc或calloc(或者realloc)分配的内存。realloc()函数需要两个参数:一个是包含地址的指针,该地址以前由malloc()、calloc()或realloc()返回,另一个参数是 要分配的新内存的字节。
realloc()函数会动态分配 第二个参数 指定的内存量,并把第一个指针参数引用的、以前分配的内容复制到新分配的内存中,当然如果旧内存(以前分配的内存)的内容长度比新内存的容量长度小,这好理解。但如果新内存的容量比旧内存的内容的长度小,则旧内容的内容会被截断的复制到新内存中。 也就是说新扩展的内存可以小于或小于原内存。
该函数返回一个指向新内存的void*指针,如果函数因某种原因失败,就返回NULL。
而且第一个参数可以为NULL,这样就类似于malloc()函数了,直接分配内存。
make a conclusion
第二个参数常常让人混淆,是扩展?,还是扩展到? 答案是“扩展到!!!”,再配合这个来记会比较好记,也就是旧内存会被自动释放,不用自己手动去释放。也就是原来的大小并没有参考性,所以第二个参数是内存的最终值,而不是增加值。
指针用作参数和返回值
在这里先理清概念:
a函数去调用b函数,则称为a为调用函数,b为被调用函数。
常量参数
在函数参数里,由于变元是按值传递的,所以只有参数是一个指针时,这个const关键字才能在函数参数生效。const关键字修饰函数参数,这表示函数将传送给参数的变元看做一个常量。
bool SendMessage(const char* pmessage){
// Code to send the message
return true;
}
参数pmessage的类型是指向const char的指针,也就是说不能修改的是char值,而不是其地址。可以看指针的“指向常量的指针”的章节,把const关键字放在开头,指定被指向的数据是常量。编译器将确认函数体中的代码没有使用pmessage指针修改消息文本。也可以把指针本体指定const,但这没有意义,因为地址是按值传递的,所以不能修改调用函数中的原始指针。
将指向常量数据的指针作为变元传递给为声明为const的参数时,C编译器会给出一条警告信息。
当参数是(指向指针的)指针时,使用它就有点复杂了。此时,传递给该参数的变元是按值传递的,就像其他变元一样,所以无法把指针指定为const。但是,可以把(指向指针的)指针定义为const,防止修改指针指向的内容。但我们仅希望最终被指向的数据时const。对于(指向指针的)指针参数,下面是const一种可能的用途:
void sort(const char** char, sizte_t n);
这是sort()函数的原型,其第一个参数指向const char*的指针类型。把第一个参数看作一个字符串数组(不是字符数组或一个字符串),则字符串本身是常量,它们的地址和数组的地址都不是常量。这是合适的,因为该函数会重新安排在数组中存储的地址,而不是修改它们指向的字符串。可以参考指针的“指向常量的指针”的章节。
第二种可能的用途是:
void replace(char *const *str,size_t n);
这里,第一个参数是(指向char的常量指针的)指针。变元是一个字符串数组,函数可以修改字符串,但不能修改数组中的地址。例如,函数可以用空格替换标点符号,但不能重新安排字符串在数组中的顺序。可以参考指针的“常量指针”的章节。
最后一种可能用途:
size_t max_length(const char* const* char, size_t n);
这就是跟指针的“极端例子”的章节一样,在这个函数原型中,第一个参数是(指向const char的常量指针)的指针类型。数组中的指针时常量,它们指向的字符串也是常量。该函数可以访问数组,但不能以任何方式修改数组。这个函数一般返回字符串的足底啊长度,获得这个数据时不会修改任何内容。
参考:C语言入门经典(第5版)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。