1)return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。例如:
char * Func(void)
{
char str[] = “hello world”; // str的内存位于栈上
…
return str; // 将导致错误
}
2)使用断言
void *memcpy(void *pvTo, const void *pvFrom, size_t size)
{
assert((pvTo != NULL) && (pvFrom != NULL)); // 使用断言
byte *pbTo = (byte *) pvTo; // 防止改变pvTo的地址
byte *pbFrom = (byte *) pvFrom; // 防止改变pvFrom的地址
while(size -- > 0 )
*pbTo ++ = *pbFrom ++ ;
return pvTo;
}
assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行,原型定义:
#include <assert.h>
void assert( int expression );
assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,
然后通过调用 abort 来终止程序运行。
3)引用和指针的比较
引用是C++中的概念,初学者容易把引用和指针混淆一起。一下程序中,n是m的一个引用(reference),m是被引用物(referent)。
int m;
int &n = m;
n相当于m的别名(绰号),对n的任何操作就是对m的操作。
4)传值
以下是“值传递”的示例程序。由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n, 所以n的值仍然是0。
void Func1(int x)
{
x = x + 10;
}
…
int n = 0;
Func1(n);
cout << “n = ” << n << endl; // n = 0
5)传地址
以下是“指针传递”的示例程序。由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10。
即实参是外部变量的地址,把外部变量的地址传给形参,形参为指针变量x,函数中执行(* x) = (* x) + 10;即通过形参数传入的地址里的数据被改变了,也即外部变量的值改变了。
void Func2(int *x)
{
(* x) = (* x) + 10;
}
…
int n = 0;
Func2(&n);
cout << “n = ” << n << endl; // n = 10
6)引用传递
以下是“引用传递”的示例程序。由于Func3函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10。
void Func3(int &x)
{
x = x + 10;
}
…
int n = 0;
Func3(n);
cout << “n = ” << n << endl; // n = 10
对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。
这时引用就是一个别名;
7)指针和数组
#include <stdio.h>
void main()
{
char a[] ="hello";
char *p = "world"; // 注意p指向常量字符串
a[0] = 'X'; //a数组内容可以改变
p[0] = "X"; // 编译器不能发现该错误
printf("%s\n",a);
}
因为p指向字符串常量的地址,字符串常量之所以称之为常量,因为它可以看作是一个没有命名的字符串且为常量。存储于静态数据区
常量就意味着它具有“只读”属性,不允许被修改,而p[0] = "X";试图修改第一个字符,所以会发生错误。
8)指针参数如何传递内存
如果函数的参数是一个指针,不要指望用该指针去申请动态内存,在下面的例子中,Test函数的语句GetMemory(str, 200)并没有使str获得期望的内存,str依旧是NULL,为什么?
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, "hello"); // 运行错误
}
原因分析:
char *str =NULL; 的含义是定义一个char型的指针str ,这个指针指向的变量值为NULL,那么GetMemory(str,100)调用时,形参会copy实参一份东东,比如str的地址是1000,而此时形参的地址是1004,而str指向的变量的地址为800,那么1004的地址为str的copy,他们都指向800;这时在函数GetMemory里面 形参p的地址为1004,然后在里面分配了一块内存,假设分配的内存的地址为2004,这时p就指向2004了。即此时实参还是指向800,而p指向了2004,函数返回时,当然str指向的还是800,此时800里面的值为NULL,所以strcpy(str,"hello")就会出错了。
每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存。
这里应该注意的地方就是指针作为参数只能改变实参的值,而上面的例子是讲形参的地址改变了,而实参的值没有变,所以是错误的。
我补充说明下:上例中str是指针变量,注意它也是变量,只不过它存储的值是另一个变量的地址而已,而不像普通变量存储的值就是一个值(数据),那么调用函数GetMemory(str, 100); 是把指针变量作实参传入,注意是传指针变量,而不是传指针变量的地址,即是像上面说的形参会copy实参一份东东,所以在GetMemory函数里在里面分配了一块内存并形参数返回,而实参并未改变,即是值传递,这里函数参数是指针类型了为什么还是值传递呢,这其实是相对函数里是怎样对传入的指针变量操作来讲的,像上面5)同样是传指针变量,指针变量里存储的是某个变量(或其他xx,5)中是int数据)的地址,即传入的是指针变量里存储的int数据n的地址,就好比是某个函数入参是int类型,则调用函数时传入的实参(变量)即是这个变量里内容,即这个变量里存储的数据,函数入参是指针变量时传入的指针变量就是这个指针变量里存储的内容,即某个变量或其他的什么东东的地址。
在5)中是传入这个指针变量给形参后,即传入的是外部变量n的地址,函数是对传入的这个地址取里面的数据并改变它,即是改变了这个外部变量的数据大小了。 而8)中str是0地址(Vc++6.0中是0地址),然后把这个0地址传入 GetMemory(char *p, int num)里,形参指针变量接收到传入0地址后函数操作是 p = (char *)malloc(sizeof(char) * num);,本来p是0地址,经过p = (char *)malloc(sizeof(char) * num);后p变成了新申请到的一块堆内存的地址了(假设申请成功),但是有什么用,呵呵,0地址还是0地址(NULL),NULL本来就存在,str还是0地址NULL,形参p自从得到传入的str的值NULL后,p= (char *)malloc(sizeof(char) * num);操作p和str再没半毛钱关系,但是如果传入的是str的地址,即指针变量str的地址,就有关系了,为分析我改了如下的测试程序:
#include<stdio.h>
char a=21;
char b=35;
char c=78;
char *my_test(char *p,char **pp)
{
p = &b;
*pp=&b;
printf("p =%d\n",*p);
printf("pp =%d\n",**pp);
return p;
}
void main(void)
{
char *str = &a;
char *strc=&c;
char **strr=&strc;
my_test(str,strr);
printf("str =%d\n",*str);
printf("strc=%d\n",**strr);
printf("strc=%d\n",c);
}
/*
说明:my_test(char *p,char **pp)函数中,形参指针变量p得到外部变量a的地址,函数里又把外部变量b的地址赋给形参指针变量p,
p指针变量嘛,可以随便指向,但p也仅仅是从开始的指向a地址到指向b地址,对外部的str指针变量来说还是指向a地址;
strc指针变量存储c地址,strr又存储指针变量strc的地址,函数my_test(char *p,char **pp)形参pp得到strr的值即指针变量strc的地址,
那么函数里*pp即取strc的值,其值为外部变量c的地址,即*pp=&b操作是把外部变量b的地址赋给外部变量c的地址,即c的地址被换成b的地址,
确切的讲是strc指针变量的指向由原来的指向外部变量c到my_test(char *p,char **pp)里执行*pp=&b指向外部变量b的地址,注意是指针变量
的值发生了变化,而不是外部变量c的地址变成b的地址。所以函数里char **pp参数直接改变了传入的实参指针变量指向值,而char *p参数只
是能改变传入的地址里的数据,而不能改变地址。
*/
9)指向指针的指针
针对8)的问题,可以采用指向指针的指针解决;
void GetMemory2(char **p, int num) { *p = (char *)malloc(sizeof(char) * num); } |
void Test2(void) { char *str = NULL; GetMemory2(&str, 100); // 注意参数是 &str,而不是str strcpy(str, "hello"); cout<< str << endl; free(str); }
|
此时:解释如下:
char *str = NULL; 和8)一样
GetMemory2(&str, 100);传递的是&str,即str的地址,在上面分析了假设str的地址为1000,那么传递的就是1000;此时,str指向的变量为NULL,即没分配地址,而形参为char **p; 当实参传递给形参时,将p=&str,即将1000传给了p,即指针变量p的值为1000,这时在函数里面使用*p来分配内存,相当于1000地址所指向的变量的内容为malloc分配的地址,这样就使得str指向的变量也为malloc的地址,从而输出正确的值。
10)用函数返回值来传递动态内存
char *GetMemory3(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
看到这里,有些同学可能会问,返回指针的值,那么p是局部变量,用完就不存在了,不会有什么问题吗?
这里大家要注意,p指向的内存是在堆上分配的,而不是在栈分配的,malloc分配的内存一直有效,而p作为局部变量,函数外就不存在了,但是此时它已经将内存传递给str了。str = GetMemory3(100); 所以是正确的。这里注意和例子1)区分开来;
11)return语句返回指向“栈内存”的指针 是错误的
char *GetString(void)
{
char p[] = "hello world";
return p; // 编译器将提出警告
}
void Test4(void)
{
char *str = NULL;
str = GetString(); // str 的内容是垃圾
cout<< str << endl;
}
因为p是局部变量,作用域在函数内,char p[] = "hello world"; 分配的内存在栈上,它只有在该函数内有效,所以返回时,内存就无效了。因此上例是错误的。
12)return语句返回常量字符串 不合常理
char *GetString2(void)
{
char *p = "hello world";
return p;
}
void Test5(void)
{
char *str = NULL;
str = GetString2();
cout<< str << endl;
}
虽然能正确输出结果,
GetString2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。
13)指针的算术运算
指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的。例如:
1、char a[20];
2、int* ptr=a;
...
...
3、ptr++;
在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整形变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做单位的,故ptr所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。
由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。
1、char a[20];
2、int* ptr=a;
...
...
3、ptr+=5;
在这个例子中,ptr被加上了5,编译器是这样处理的:将指针ptr的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20。由于地址的单位是字节,故现在的ptr所指向的地址比起加5后的ptr所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ptr指向数组a的第0号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。
14)指针的强制类型转换
1、floatf=12.3;
2、float*fptr=&f;
3、int*p;
在上面的例子中,假如我们想让指针p指向实数f,应该怎么搞?是用下面的语句吗?
p=&f;
不对。因为指针p的类型是int*,它指向的类型是int。表达式&f的结果是一个指针,指针的类型是float*,它指向的类型是float。两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行 "强制类型转换 ":
p=(int*)&f;
如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP*TYPE, 那么语法格式是:
(TYPE*)p;
这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE*,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。而原来的指针p的一切属性都没有被修改。
函数中指针做形参,强制类型转换例子。
voidfun(char*);
inta=125,b;
fun((char*)&a);
...
...
voidfun(char*s)
{
charc;
c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;
c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;
}
}
注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实参&a的结果是一个指针,它的类型是int*,它指向的类型是int。形参这个指针的类型是char*,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int*类型到char*类型的转换。结合这个例子,我们可以这样来想象编译器进行转换的过程:编译器先构造一个临时指针char*temp, 然后执行temp=(char*)&a,最后再把temp的值传递给s。所以最后的结果是:s的类型是char*,它指向的类型是char,它指向的地址就是a的首地址。
15)整数可以赋值给指针吗?
我们已经知道,指针的值就是指针指向的地址,在32位程序中,指针的值其实是一个32位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句:
unsignedinta; TYPE*ptr;//TYPE是int,char或结构类型等等类型。 ... ... a=20345686; ptr=20345686;//我们的目的是要使指针ptr指向地址20345686(十进制 ) ptr=a;//我们的目的是要使指针ptr指向地址20345686(十进制) 编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到了吗?不,还有办法: unsignedinta; TYPE*ptr;//TYPE是int,char或结构类型等等类型。 ... ... a=某个数,这个数必须代表一个合法的地址; ptr=(TYPE*)a;//呵呵,这就可以了。 严格说来这里的(TYPE*)和指针类型转换中的(TYPE*)还不一样。这里的(TYPE*)的意思是把无符号整数a的值当作一个地址来看待。上面强调了a的值必须代表一个合法的地址,否则的话,在你使用ptr的时候,就会出现非法操作错误。 想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完 全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针: 例十六: inta=123,b; int*ptr=&a; char*str; b=(int)ptr;//把指针ptr的值当作一个整数取出来。 str=(char*)b;//把这个整数的值当作一个地址赋给指针str。 现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。 指针的安全问题 看下面的例子: 例十七: chars= 'a '; int*ptr; ptr=(int*)&s; *ptr=1298; 指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的首地址。在32位程序中,s占一个字节,int类型占四个字节。最后一条语句不但改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。 让我们再来看一例: 例十八: 1、chara; 2、int*ptr=&a; ... ... 3、ptr++; 4、*ptr=115; 该例子完全可以通过编译,并能执行。但是看到没有?第3句对指针ptr进行自加1运算后,ptr指向了和整形变量a相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。而第4句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。 在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是不安全的。至于为什么,读者结合例十七来想一想,应该会明白的。 |
下面看及格简单的例子,可以说明他们的区别:
第一个
2 char * str1 = { " Hello " };
3 char * str2 = { " Hello World " };
4 char * const ptr1 = str1 ;
5 // 指针常量--指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化
6
7 ptr1 = str2; // 错误 因为这是一个常量指针,改变指向的地址了
8
9 printf( " %s \n " , * ptr1);
10 }
11
12
13 // 编译错误 error C3892: 'ptr1' : you cannot assign to a variable that is const
14
</>第二个
2 char * str1 = { " Hello " };
3 char * str2 = { " Hello World " };
4 char * const ptr1 = str1 ;
5 // 指针常量--指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化
6
7 * ptr1 = ' A ' ; // 正确 因为指向的地址的内容是可以变化的
8
9 printf( " %c \n " , * ptr1);
10 }
11
12 // 输出 A
13
第三个
2 char * str1 = { " Hello " };
3 char * str2 = { " Hello World " };
4 const char * ptr1 = str1;
5 // 常量指针--指向字符串常量,所指向的字符串内容不能变,但是指向的地址可以变化
6
7 ptr1 = str2; // 正确 因为指向的地址是可以变化的
8
9 printf( " %s \n " ,ptr1);
10 }
11
12 // 输出 Hello World
第四个
2 char * str1 = { " Hello " };
3 char * str2 = { " Hello World " };
4 const char * ptr1 = str2;
5 // 常量指针--指向字符串常量,所指向的字符串内容不能变,但是指向的地址可以变化
6
7 ptr1 = ' A ' ; // 错误 因为指向的地址是内容是不可以变化的
8
9 printf( " %c \n " ,ptr1);
10 }
11
12
13 // 编译错误 error C2440: '=' : cannot convert from 'char' to 'const char *'
14
相信从上面四个简单的例子可以看出他们不一样的地方把,在这里要请大家注意一下的地方是:
指针常量的申明:const 放在* 和指针名之间 Type * const pointer ;
常量指针的申明:const放在类型说明符之前 const Type *pointer ;
const右边是指针,那么首先这是一个指针,所以是常量指针。如char const *pointer;或者const char *pointer;这个指针指向一个常量,所以它指向的地址上的值是不能变的,当然它的指向是可以变的,即这个指针的值是可以变的,但所指向的内容是不能变的。
const右边是变量,那么这是一个常量,就是指针常量。如char *const pointer;常量不可变,这是一个指针性质的常量,指针实际上就是一个地址值,由于它是常量,所以这个地址值不能变,即指针常量所指的位置不能变,但所指的位置上的值是可以变的。