指针
指针就是内存地址,用于间接访问内存单元。
指针变量:存放地址的变量。
回顾一下,我们之前学过的,跟地址有关系的内容:
1. 引用,在函数参数传递时,可以使用引用作为形参,也就是传变量的地址,这样对于大对象能够提高效率。
2. 数组,在定义数组时,数组名其实就是数组第一个元素的地址。
1. 与指针有关的基本操作
1.1 指针变量的初始化和赋值
初始化的语法为:
存储类型 指针指向的类型* 指针名 = 初始地址
其中初始地址原则上要是程序运行中合法获取的地址,也就是说:要么通过取址运算获得;要么通过动态分配对象内存获得。
看如下的代码示例:
//
// main.cpp
// Pointer
//
// Created by Evelyn on 2018/8/9.
// Copyright © 2018年 Evelyn. All rights reserved.
//
#include <iostream>
using namespace std;
int main(int argc, const char * argv[]) {
static int i = 1;
//int说明指针变量ptr只想的变量的类型,是为了从内存中合法的存取数据;
//*说明正在定义的变量是一个指针变量;
//&在此处不再是引用符,而是是取址操作符
static int* ptr = &i;
cout << "i: "<< i << endl;
//“*指针名”表示寻址操作,也就是指针所指向的地址中的数据
cout << "*ptr: " << *ptr << endl;
cout << "ptr: " <<ptr << endl;
cout << "&i: " << &i << endl;
cout << "-------re-assignment---------" << endl;
*ptr = 3;
cout << "i: "<< i << endl;
cout << "*ptr: " << *ptr << endl;
cout << "ptr: " <<ptr << endl;
cout << "&i: " << &i << endl;
//void指针
void* ptr2 = &i;
int* ptr3 = static_cast<int*> (ptr2);
cout << "*ptr3: " << *ptr3 << endl;
//指向常量的指针
const int* ptr4 = &i;
i = 5; //合法
//指针类型的常量
int* const ptr5 = &i;
*ptr5 = 6;
return 0;
}
运行结果:
i: 1
*ptr: 1
ptr: 0x1000020d0
&i: 0x1000020d0
-------re-assignment---------
i: 3
*ptr: 3
ptr: 0x1000020d0
&i: 0x1000020d0
*ptr3: 3
其中定义了静态局部变量i,属于int型变量,在内存中占四个字节,ptr指向i的意思就是把i的地址赋给ptr。它们在内存中的关系如下图所示。
从程序的运行结果,也能说明这个关系。
对指针变量的赋值一般是指对指针变量指向的对象赋值,语法格式为:
*指针名 = 赋值内容
还是上一个示例程序,我们看到,对指针变量重新赋值,首先使用*寻址操作,也就是说,取出ptr中保存的地址,然后根据这个地址找到内存中对应的变量,最后用=右侧的内容给这个变量赋值。重新赋值前后,i的值变了,但是其地址没有变。
1.2 void指针
声明指针的数据类型,也就是声明指针指向的数据类型,那么void指针是允许存在的,并且可以给他赋值。但是不能通过这个指针来访问它指向的数据或对象,即不可读也不可写。
void指针是一类特殊的指针,它只能存放地址,而不说明指向的内存对象的数据类型,因而不能访问其指向的对象,但是通过强制转换,可以再次访问内存对象,例如下面的代码: void* ptr2 = &i;
int* ptr3 = static_cast<int*> (ptr2);
1.3 指向常量的指针
指向常量的指针换一种说法就是只读指针,也就是说,不能通过指针的寻址操作来改变它指向的对象或数据的值,但是至于它指向的对象被声明为常量与否,则不一定。语法为:
const 数据类型* 指针名 = 地址
1.4 指针类型的常量
指针类型的常量其实就是指针本身是一个常量,不能被改变。但是它指向的值可以被改变。也就是说这个指针只能指向这一块内存单元,必须初始化,语法为:
数据类型* const 指针名 = 地址
示例代码如下: //指向常量的指针
const int* ptr4 = &i;
i = 5;
//指针类型的常量
int* const ptr5 = &i;
2. 指针与数组
既然数组名就是数组的第一个元素的地址,若我们定义了 int a[] = {1, 2, 3};
int* p = a;
那么以下表达式都为真: ptr == &a[0];
ptr == a;
*(ptr + 1) == a[1]
ptr[0] = a[0];
我们来看以下使用指针访问二维数组的例子:
//
// main.cpp
// PointerOfArray
//
// Created by Evelyn on 2018/8/11.
// Copyright © 2018年 Evelyn. All rights reserved.
//
#include <iostream>
int main(int argc, const char * argv[]) {
using namespace std;
//define three row vertex
int line0[] = {1, 0, 0};
int line1[] = {0, 1, 0};
int line2[] = {0, 0, 1};
//define one two-dimention array
int* ptr[3] = {line0, line1, line2};
//print this array
for(int j = 0; j < 3; j++) {
for(int i = 0; i < 3; i++ ) {
//使用ptr指针访问二维数组
cout << ptr[j][i] << " ";
}
cout << endl;
}
return 0;
}
输出结果为:
1 0 0
0 1 0
0 0 1
我们看到,使用ptr和数组下标也能正确的访问整个二维数组,那么这样的二维数组和先前方法定义的a[3][3]有什么区别呢?
在c++代码层面上,它们的使用没有区别,但是在存储方式上,上一篇文章中已经提到,数组元素在内存中是连续存储的,那么a[3][3]就是在内存中分配一块4*9 = 36 字节的连续空间,而本例中,只需要分配3个4*3 = 12 字节的连续内存空间就可以了,至于这3个12字节的空间在内存中的相互位置,则没有要求。
3. 指针作为函数参数
本文开头已经提到了,传引用就是穿地址,也就是指针。因此指针完全可以代替引用作为函数的参数传递。
需要指针作为函数参数的情形:
1. 需要对数据进行双向传递
2. 需要传大的对象或者传一组数据时,只需给一个起始的指针,能够提高效率
4. 指针类型的函数
以指针作为返回值的函数就是指针类型的函数。指针类型的函数最容易出错的一点就是将非静态局部地址用做函数的返回值,这一点和基本数据类型作为返回值的函数很大不同。虽然在语法上,这种程序能够通过编译,并且能够运行,但是存在很大的安全隐患,因为函数中非静态局部变量在退出函数后就会被释放其内存空间,并且很可能被分配给其他的进程来使用,这时候,这个返回的指针指向的内容就难以确定。
正确做法:
1. 可以在被调函数中定义静态局部变量作为返回值
2. 可以使用new在被调函数中为返回值申请内存
看以下示例:
//
// main.cpp
// PointerAsArg
//
// Created by Evelyn on 2018/8/11.
// Copyright © 2018年 Evelyn. All rights reserved.
//
#include <iostream>
using namespace std;
int* func();
int main(int argc, const char * argv[]) {
int* ptr = func();
//Dangerous. aAddress of stack memory associated with local variable 'local' returned
*ptr = 5;
cout << *ptr;
return 0;
}
int* func() {
int local = 0;
return &local;
}
func()函数运行结束时,local变量被释放,此时,main函数中又给这个地址赋值为5,属于危险访问,因为这个地址很可能已经被分配给其他程序了。
修改方式1:
//
// main.cpp
// PointerAsArg
//
// Created by Evelyn on 2018/8/11.
// Copyright © 2018年 Evelyn. All rights reserved.
//
#include <iostream>
using namespace std;
int* func();
int main(int argc, const char * argv[]) {
int* ptr = func();
//Dangerous. aAddress of stack memory associated with local variable 'local' returned
*ptr = 5;
cout << *ptr;
return 0;
}
int* func() {
//将local声明为静态变量
static int local = 0;
return &local;
}
修改方法2
//
// main.cpp
// PointerAsArg
//
// Created by Evelyn on 2018/8/11.
// Copyright © 2018年 Evelyn. All rights reserved.
//
#include <iostream>
using namespace std;
int* func();
int main(int argc, const char * argv[]) {
int* ptr = func();
//Dangerous. aAddress of stack memory associated with local variable 'local' returned
*ptr = 5;
cout << *ptr;
return 0;
}
int* func() {
//使用new来申请内存分配
int* local = new int(0);
return local;
}
5. 对象指针
对象指针的声明和基本数据类型变量的声明是相同的,不同的是,对象有它的成员,在访问对象的成员时,我们学过的是: 对象名.成员名
那么用对象指针时,应该用: 指针名->成员名