一,指针
1,指针的含义
指针是一种特殊的遍历,其存储的值被解释成内存里的一个地址。要理解一个指针需要搞清指针的四方面内容:指针的类型,指针所指向的类型,指针的值(指针所指向的内存地址),指针本身所占据的内存地址。
- 指针的类型
从语法的角度说,指针的类型,只要把指针声明语句中的指针名去掉,剩下的部分就是指针类型。例如:
int *ptr; //指针类型是int*
int **ptr; //指针类型是int**
int (*ptr)[3]; //指针类型是int(*)[3]
int*(*ptr)[4]; //指针类型是int*(*)[4]
//注意
int*ptr[5] //ptr是个数组,数组的类型是int*, 大小为5
- 指针所指向的类型
当通过指针来访问其指向的内存时,其所指向的类型决定了编译器将那边内存区的内存当作什么来看。从语法的角度说,指针指向的类型,就是把指针声明语句中的指针名和名字左边的指针声明符*去掉。剩下的就是指针所指向的类型。例如:
int *ptr; //指针所指向的类型是int
int **ptr; //指针所指向类型是int*
int (*ptr)[3]; //指针所指向类型是int[3], ()只是为了改变优先级,可以忽略
int*(*ptr)[4]; //指针所指向类型是int*[4], ()只是为了改变优先级,可以忽略
在指针的算术运算中,指针所指向的类型又很大的作用。
- 指针的值(所指向的内存地址)
指针的值就是指针本身存储的数值,这个值被编译器当作一个地址,而不是一个数值,32位程序保存的是32位整数,64位程序保存的是64位整数。所指向的内存区域是指从其所指地址为起始地址,长度为sizeof(所指类型)的一块内存。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。
- 指针本身所占据的内存地址
指针本身所占的内存大小可以通过关键字sizeof(指针的类型)获得,32位平台占4个字节。64位占8个字节。
2,指针的算术运算
指针的算术运算支持加,减一个整数操作。指针的这种运算与一般的运算不一样,是以单元为单位。单元的长度是指其所指向的类型。例如:
int array[20] = {0};
int *ptr = array;
int **ptr1 = &ptr;
(*ptr)++; //*ptr取值后+1, 即其所指向的内存的值+1;
ptr++; //ptr+1,ptr所指向的类型是int(4个字节),所以ptr的值(地址)+4,即指向下一个array元素;
ptr1++; //ptr1+1,ptr1所指向的类型是int*(4个字节),所以ptr1的值+4,指向无效的一块内存地址;
3,指针与数组的关系
数组名的说明:有两重含义:一是代表整个数组,它的类型是TYPE[n],所以sizeof(array)中array表示整个数组本身的大小。二是一个常量指针,即指针的值不能修改的,类似array++的表达式是错误的,array+n的结果是个指针并没有改变array本身的值,因此没有问题, sizeof*array/sizeof(*array)表示数组元素的大小。
需要注意的是对于指针数组的是不同的。指针名的含义代表自己本身,不代表所指内存。例如
int array[10];
int (*ptr)[10];
ptr = &array;
在32位系统下:
sizeof(array) == 40;
sizeof(int(*)[10]) == 4;
sizeof(int[10]) == 40;
sizeof(ptr) == 4;
说明:sizeof(对象)测出的都是对象的自身类型的大小,而不是别的类型的大小。
sizeof(类型)测出该类型的大小。
4,指针类型转换
float f=12.3; float* fptr = &f; int*p =(int*)fptr;
对于类型转换的原指针(fptr),其一切属性都没有被改变(切记),对于p的类型时int*,其值指向了fptr同一地址,指向的类型时int。
指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。
5,复杂类型的说明
要了解指针,多多少少会出现一些复杂的类型,准确分析类型,就了解了指针,要理解复杂的类型很简单,一个类型里也会出现很多运算符,像分析表达式一样,有优先级,其优先级和运算优先级一样。所以遵循其下原则:从变量名处其,根据运算符优先级结合,一步步分析,例如:
int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针
int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组
int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组
int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针
int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.
int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据
Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针
int *(*p(int))[3]; //从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.
5,使用说明
不要返回函数体内new
二,引用
1,引用的初始化
引用的声明方法:类型标识符 &引用名=目标变量名;
c++引入的新语言特性,引用:为变量提供别名,而不是定义一个变量,因此引用本身并不占用内存,而是和目标变量共同指向目标变量的内存地址。(因此要留意目标变量的生命周期问题)
定义一个引用时,必须对其初始化。对已经初始化的应用,不能再把该引用作为其他变量名的别名(即不能被修改);
引用可以理解为,一个不可以改变指向地址的指针。
2,引用作为参数
在c语言中函数的参数传递,是值传递和地址传递,地址传递即传入地址,避免整块数据全部压栈。c++中引入的引用。引用作为函数形参时,不需要初始化,在调用时完成初始化。
使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作。值传递和地址传递都会产生副本。
如果要保护传递给函数的数据不在函数体中被修改,需要使用常引用:
1):常引用声明方式:const 类型标识符 &引用名 = 目标变量名;
2):常引用的意义:不能修改引用本体的值。但本体自身可以修改,前提本体是非const类型。
string str = “hi”;
const string str1 = "hello";
const string &tmp = str;
tmp = "world"; //error 禁止通过常引用修改本体的值
str = “hi world”; //编译通过
const string & tmp = str ; // 绑定到非 const 对象的 const 引用
tmp = "world"; // 错误:试图通过到 const 的引用修改
const_cast<string &>(world) = "world"; // ok,修改非 const 对象 n1
const string & tmp1= str1 ; // 绑定到 const 对象的 const 引用
tmp1 = "world"; // 错误:试图通过到 const 的引用修改
const_cast<string &>(tmp1 ) = "world"; // 未定义行为:试图修改 const 对象 n2
在C++中,临时对象都是const类型的,所以引用型参数应该在能被定义为const情况下,尽量定义为常引用。
string foo();
void bar(string &s);
//下面的两个表达式都是非法的,试图将const类型对象传给非const类型对象
bar(foo());
bar("hello world");
3,引用作为返回值
引用作为函数返回值的最大好处就是,在内存中不产生被返回值的副本。引用作为返回值需要注意如下:
- 不能返回局部变量的引用
主要原因是局部变量会在函数返回后被销毁。
- 不能返回函数内部new分配的内存的指针所指对象的引用
准确的说不是不能,而是为了避免不必要的麻烦,预防内存泄漏,每个用new产生的指针必须保证会调用delete释放,返回指针的引用很容易出现函数中使用了new,返回的是new初始化的指针所指向的对象的引用,在函数体外忘记delete,找出内存泄漏。例如:
string& foo(){
string *str = new string("hi");
return *str;
}
//要想释放str,必须如下操作
string &tmp = foo();
delete &tmp;
这样就不会造成内存泄露了。 但是每次的这样就是谁都觉得烦。而且暗藏杀机啊,比如:string str = "hello" + foo(); 上式不知不觉就造成内存泄露了。所以,即使很小心的程序员也难免会造成内存泄露。因此不提倡这样使用。
- 可以返回类成员的引用,但最好时const。
- 返回引用的函数返回值可以作为左值,这个特性可以用于流操作符<<,>>和赋值操作符 =(a=b=c,连等)的重载。但在基本的四则运算符重载不能返回引用。
4,引用和多态
引用时除指针外另一个可以产生多态效果的手段,这意味着,一个基类的引用可以指向它的派生类实例。
class B{
public:
virtual foo(){cout<<"B::foo()"<<endl;}
}
class A : public class B{
public:
foo(){cout<<"A::foo()"<<endl;}
}
A son;
B& father = son;
father.foo(); //A::foo()
另外,函数参数引用基类,可以在函数调用时引用派生类,实现多态。
三,指针和引用的比较
从c++角度,引用和指针没有任何关系。引用可以理解为,一个不可以改变指向地址的指针,即引用是一次性的指针。