Skip to content

Latest commit

 

History

History
245 lines (196 loc) · 9.39 KB

File metadata and controls

245 lines (196 loc) · 9.39 KB

c++ 指针

1. 地址

C++ primer plus 中举了一个例子,依次定义一个int、double,将它们的地址打印出来,地址分别是两个16进制数,0x0065fd40 和 0x0065fd44,表示两个变量在内存中的地址,两个地址的差为0x0065fd44 – 0x0065fd40(即4)。这是有意义的,因为int类型使用4个字节。

2. 指针类型

可能有些人会存在误解,认为指针类型(指针)就是地址,其实这么说不严谨,正确说法是指针类型是存储地址的数据类型。它有两个特性,一个是存储变量地址,另一是通过地址获得变量的值。比如

int ducks = 12;
int* duck_ptr = &ducks;
std::cout << duck_ptr << std::endl; // 0x61ff18
std::cout << *duck_ptr << std::endl; // 12

直接打印一个指针类型,得到的是一个十六进制的数,这个数是变量ducks的地址。通过前面加个*得到地址对应的变量的值。

指针类型的*普遍有两种写法,int* ptrint *ptr,个人更喜欢前者,因为这让人觉得这是一个int的指针类型,而不是单纯的int类型。

20190309173505.png

2.1 指针的初始化

当你定义一个指针类型,就一定要赋予他一个地址,否则你不知道它指向哪里。如果你不小心修改地址对应的值,就会产生错误。

long* f;
*f = 23333;

上面这种写法会导致难以追踪的错误。

2.2 new

c里面可以用malloc分配内存,c++中可以使用new。

int* p = new int;

new int告诉程序,需要适合存储int的内存。new运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。

2.3 delete

使用delete可以释放指针类型所指向的内存。

int* ps = new int;
delete ps;

这将释放ps指向的内存,但不会删除指针ps本身,可以将ps重新指向另一个新分配的内存块。

注意,凡是使用了new分配的内存,一定要使用delete将其释放。因为这部分是你手动分配的,计算机不会自以为是的将它们回收,那么你也必须手动将其回收。

3. 动态数组

可以使用指针当作数组来用,指针将指向“数组”开始的位置。注意这里指针并非真正的数组,指针只是指向了数组的第一个元素。

而指针能够通过下标访问元素是指针本身便具有的功能,与该指针指向变量还是数组无关。

int *ps = new int[10];
ps[0] = 10;
ps[1] = 11;
delete [] ps;

与真正的数组不同的是。你可以对指针类型进行加减。比如,加1后,指针的将指向第二个位置,如果是整型,即地址加了4,同时指针的开始位置将指向“数组”的第二个元素。当删除的时候,你就要把指针移回一开始的位置再删除,不然会内存泄漏。

ps = ps + 1;
// ...
ps = ps - 1;
delete [] ps;

3.1 删除问题

如果使用new时,不带方括号,则使用delete时,也不应带方括号。如果使用new时带方括号,则使用delete时也应带方括号。

C++ Prime Plus 中提到:

如果使用new [ ]为一个实体分配内存,则应使用delete(没有方括号)来释放。(这里的实体应该是指“对象”)

我特地搜了一下,有人说这句话不太对,但实际上不影响,编译器的防呆设计可以消除这种差异,详见知乎。事实上,编译器在你创建一个数组或动态数组的时候都会在一个地方记录数组的大小,不同编译器记录的方式可能不同。

3.2 指针数组 和 数组指针

指针数组,即数组的每个元素的类型都是一个指针。

int a = 1, b = 2, c = 3;
int* p[3];  // 初始化一个3个元素的数组,元素类型是int*
p[0] = &a; p[1] = &b; p[2] = &c;

cout << *(p[0]) << endl; // 1
cout << *p[0] << endl; // 1

注意,下标的优先级高于*

数组指针,也叫指向数组的指针。上面写到的“动态数组”就是一种数组指针,表示一个指针变量指向一个数组。复杂一点是二维数组指针。

int arr[2][3] = {{ 0, 1, 2 },{ 3, 4, 5 }};
int (*q)[3];
q = arr;

上面这种写法是二维数组指针的写法,其中q可以理解为一个指向arr第一个元素的指针,而后面的[3]则表示这个元素是一个长度为3的数组。

这两个名词很容易搞混,当你知道两个名词所表示的东西后就不应该再用这两个名词去形容了,因为可能过段时间你就忘记谁对应谁了,要么是元素类型是指针,要么是指向数组的指针,但把话说清楚肯定不会产生歧义。

3.3 二维数组 与 二维指针

想把二维数组转为二维指针有下面这三种方法。

int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
const int row = 2, col = 3;

// 方法一:二维指针
int** a2d = new int*[row];
for(int i = 0; i < row; i++){
    a2d[i] = new int[col];
}

for(int i = 0; i < row; i++){
    for(int j = 0; j < col; j++){
        a2d[i][j] = arr[i][j];
    }
}

// 方法二:指针数组
int* a2d2[row];
for(int i = 0; i < row; i++){
    a2d2[i] = arr[i];
}

// 方法三:数组指针
int (*a2d3)[col];
a2d3 = arr;

4. 动态结构

当你要通过指针访问动态结构的成员时有两种办法,一种是用箭头->访问,另一种是加上*转换为实际对象再通过点号访问。

struct Node{
    int key;
    int value;
};

Node* pn = new Node{0, 1};
std::cout << pn->value << std::endl;    // 1
std::cout << (*pn).value << std::endl;  // 1

5. 存储类型

5.1 自动存储

直接赋值的变量值都存储在被称为(stack)的内存区域中,其存储持续性为自动,简称自动存储,其变量称为自动变量,自动变量的作用域在代码块内,超出代码块就会被回收。

自动变量通常存储在栈中。这意味着执行代码块时,其中的变量将依次加入到栈中,而在离开代码块时,将按相反的顺序释放这些变量。

5.2 静态存储

静态存储是整个程序执行期间都存在的存储方式。

static double fee = 5.3;

5.3 动态存储

new从被称为(heap)或自由存储区(free store)的内存区域分配内存。

数据的生命周期不完全受程序或函数的生存时间控制,也不受作用域控制,完全由程序员掌管。

通俗的讲,就是如果你new了一个变量,但不去delete它,无论走到哪个代码块中,它都会一直留在内存当中。

6. 引用

当你把变量a赋给一个引用变量b后,就相当于给a起了一个为b别名。当a或b改变时,b或a也会改变,本质上,它们是使用同一块内存空间的。

int a = 10;
int &b = a;

引用的底层实现是指针,可以看作是编译器自动完成取地址、解引用的常量指针。

6.1 引用的特点

第一点:指针可以为空,引用不能为空。

void fun1(int *point) {
     // 为了代码的稳健和安全,我们需要判断指针是否有效,通常做法是判断指针是否为
     // 空,其他的判断就需要根据函数的具体功能来判断了
     if(!point) {
        return;
     }
}

void fun2(int &refence) {
     // 在这里,我们就不用担心refence是否为空
}

第二点:避免产生临时变量。当你往函数中传入一个指针变量或者自动变量,都会在函数中产生一个拷贝。而当函数参数是一个引用类型时,就会产生引用传递,函数内部的值改变,函数外部的值也会改变。

那传指针为什么也是拷贝?虽然指针被拷贝了一份,但其指向的值不会被拷贝,你仍然是通过指针访问或改变其指向的值。

void fun1(Object obj) {
     // 此函数声明中,obj是值传递,会产生一个临时对象
}

void fun2(Object &obj) {
    // 使用引用传递,可以避免临时对象
}

第三点:指针可以为空并允许多次赋值,而引用必须初始化,且初始化后便不能改变。

int xiaoming = 1;
int &refence_mingming = xiaoming;
int xiaoan = 2;
refence_mingming = xiaoan;  // error, 引用不能换了

7 指针,引用与常量

指针搭配常量有两种:

  • 指向常量的指针
  • 常量指针

记忆方法只有一条:const后面的类型是常量。

const int a = 100;
const int* p1 = &a; // 指向常量的指针(*p1 是常量)
int const *p2 = &a; // 同上

const int a2 = 100;
p1 = &a2;   // 允许修改指针

int b = 101;
int* const p3 = &b; // 常量指针(p3 是常量)

int b2 = 101;
p3 = &b2;   // error 不能修改常量指针

两种合在一起就是指向常量的常量指针。

若想引用一个常量,则需要用一个常量引用类型接收。

const int a = 100;
const int& ay = a;

还有一种是指针类型的引用,前面说过,普通变量用来存储值,而指针变量用来一种存储地址,引用指针变量和引用普通变量没有本质区别,引用普通变量可以修改值,而引用指针变量则可以修改地址。

int* a = new int(4);
int*& ar = a;

ar = new int(5); // 直接换一个新的地址
cout << *a << endl; // 5
cout << *ar << endl; // 5

*a = 88;  // 直接修改地址所在的内存
cout << *a << endl; // 88
cout << *ar << endl; // 88

而其他&*&&等写法都是不存在的。