Overview
1.addressing 变量的寻址
2.The address-of operator (&) 取地址运算符 (&)
3.The dereference operator (*) 解引用运算符 (*)
4.Pointer initialization 指针初始化
5.Pointers and assignment 指针相关的赋值操作
6.Pointers v.s. lvalue references 指针和左值引用的对比
addressing 变量的寻址
1.对于一个 char 类型的变量 x, 使用了 1 个 bytes 的内存; 例如当执行如下的变量定义的时候, RAM 里面的一小片内存被分配给了x object; 假设 x 被分配的内存地址是 140, 当我们用到 x 的时候, 程序会去到地址 140 来获取在 140 存储的值;
2.对于变量, 我们通常不需要知道每个变量被具体分配到了内存的地址是什么, 用了多少个bytes去存存储它; 我们只是通过它的名字在使用它, 并且我们知道”它是有个确定的地址”的, 本质上是编译器完成了从”变量的标识符”到”内存的具体地址”之间的”翻译”, 这个映射地址的过程 (addressing, 寻址), 完全被编译器托管
char x{};
3.我们之前谈到的左值引用, 是完全同样的道理, 声明一个引用变量 ref, 本质上就是绑定 140 这个地址, 当我们使用引用变量 ref 的时候, 去获取140地址上储存value
int main() {
char x{}; // assume this is assigned memory address 140
char& ref{x}; // ref is an lvalue reference to x (when used with a type, & means lvalue reference)
return 0;
}
The address-of operator (&) 取地址运算符(&)
虽然通常变量的内存地址不显示的暴露出来, 当时我们如果我们非要去地址, 可以用取地址符
#include <iostream>
int main() {
int x{5};
std::cout << x << '\n'; // print the value of variable x
std::cout << &x << '\n'; // print the memory address of variable x
return 0;
}
输出的结果是
5
0027FEA0 // 是个十六进制的地址
The dereference operator (*) 解引用操作符
1.对一个变量来说, 获取变量的地址本身没有什么用处, 关于地址上我们能做的最有用事还是还是获取存储在地址上的变量; 我们可以有了变量的地址的时候, 用 dereference operator (解引用运算符) 存储在某个地址上的值
2.取地址运算符 “&” 和 解引用运算符”*” 这两个关于地址的运算符, 类似一种逆运算一样, 对于一个变量来说, 一个可以拿到x的地址, 一个可以拿到x地址下的值
#include <iostream>
int main() {
int x{5};
std::cout << x << '\n'; // print the value of variable x
std::cout << &x << '\n'; // print the memory address of variable x
std::cout << *(&x) << '\n'; // print the value at the memory address of variable x (parentheses not required, but make it easier to read)
return 0;
}
Pointers 指针
A pointer is an object that holds a memory address (typically of another variable) as its value. This allows us to store the address of some other object to use later.
In modern C++, the pointers we are talking about here are sometimes called “raw pointers” or “dumb pointers”, to help differentiate them from “smart pointers”.
intuitively,
1.Pointer (指针) 是一个 object, 这个 object 的值保存的是一个其他变量的内存地址; 这里可以对比下 reference 引用, 引用不是一个 object
2.现代 cpp 里面, 我们通常谈到的指针通常叫做 “raw pointers” 或者 “dumb pointers” , 主要是区别于智能指针 “smart pointers”
3.指针类型的声明和指针的dereference, 用的是相同的符号 *, 比如int *; 这里其实是为什么指针有时候让人迷惑的第一个原因, 有时候 * 作为一种 dereference 作用去声明, 有时候作为一种类型定义的符号出现, 所以在理解这个概念的时候, 要明确分清楚代表的不同的具体的含义
4.在同一行代码中声明2个指针类型的变量的时候, 需要将类型定义必须分开, 否则第二个变量会声明成int类型; 或者只要定义指针, 就每个指针变量单独写一行避免其他的问题
int; // a normal int
int&; // an lvalue reference to an int value
int*; // a pointer to an int value (holds the address of an integer value)
int* ptr1, ptr2; // incorrect: ptr1 is a pointer to an int, but ptr2 is just a plain int!
int* ptr3, * ptr4; // correct: ptr3 and ptr4 are both pointers to an int
Pointer initialization 指针初始化
intuitively,
1.相比一般的变量 (或者引用) , 指针的初始化需要非常小心地去理解和使用
2.和一般变量类似, 指针可以默认不被初始化的, 但是不初始化指针可能会带来相应的风险
3.一个没有被初始化的指针叫做 wild pointer (野指针), wild pointer 包含一个 garbage address (垃圾地址), 对一个 wild pointer 进行 dereference 操作会导致 undefined behavior (未定义的行为)
4.因为 wild pointer 的操作可能引起未定义的行为, 所以我们在使用的时候 always initialize pointers. 不管编译器允不允许定义野指针, 我们必须初始化指针
5.指针为什么叫”指针”? 比如指针变量 ptr 保存的是变量 x 的地址, 我们说 ptr “指向了” x
6.我们通常一种说法叫做 “int指针” “char指针” 等 “xx指针” 其中 xx 是一种类型, 这种说法是 “pointer to an x” 的一种缩写
int main() {
int x{5};
int* ptr; // 声明一个未初始化的指针, 但是不指向任何变量
int* ptr2{}; // 一个null pointer
int* ptr3{&x}; // 指向x的指针, 用x的地址进行指针的初始化
std::cout << *ptr3 << '\n'; // 用deference符号去获取ptr指向的值
return 0;
}
ptr3 执行的原理如下图
7.指针和引用有个相似点: 指针指向的变量类型和指针的类型必须要匹配上 (同理引用变量类型和被引用的类型)
int main() {
int i{5};
double d{7.0};
int* iPtr{&i}; // ok: a pointer to an int can point to an int object
int* iPtr2{&d}; // not okay: a pointer to an int can't point to a double object
double* dPtr{&d}; // ok: a pointer to a double can point to a double object
double* dPtr2{&i}; // not okay: a pointer to a double can't point to an int object
return 0;
}
Pointers and assignment 指针相关的赋值操作
有了指针之后, 我们的赋值操作从此多了一种方法: 利用指针进行赋值, 具体而言, 我们利用指针进行赋值两种赋值的方法
1.改变的指针指向的地址, 也就是维指针分配新地址; 这种情况我们的操作是
int x{3};
int y{5};
int* ptr;
ptr = &x;
ptr = &y;
2.改变的是被指向的值, 也就是分配给指针指向的值一个新的值; 这种情况我们采用的是 dereference 运算符直接赋值 *ptr = 5
int x{3};
int* ptr;
*ptr = &x;
*ptr = 5;
第 1 种情况下, 我们是修改了指针的地址
#include <iostream>
int main() {
int x{5};
int* ptr{&x}; // ptr一开始指向了x
std::cout << *ptr << '\n'; // print the value at the address being pointed to (x's address)
int y{6};
ptr = &y; // ptr重新指向了y
std::cout << *ptr << '\n'; // print the value at the address being pointed to (y's address)
return 0;
}
打印出来的结果是
5
6
第 2 种情况下, 我们直接指针的地址上的值
#include <iostream>
int main() {
int x{5};
int* ptr{&x}; // initialize ptr with address of variable x
std::cout << x << '\n'; // print x's value
std::cout << *ptr << '\n'; // print the value at the address that ptr is holding (x's address)
*ptr = 6; // The object at the address held by ptr (x) assigned value 6 (note that ptr is dereferenced here)
std::cout << x << '\n';
std::cout << *ptr << '\n'; // print the value at the address that ptr is holding (x's address)
return 0;
}
打印出来的结果是
5
5
6
6
Pointers v.s. lvalue references 指针和引用的对比
指针和引用的相似点:
1.指针和引用在很多情况下使用的时候是非常类似的, 本质都是通过绑定变量实现
2.指针和引用在对同一个变量混用的时候, 实现的赋值操作是等效的, 可以通过下面的case理解
指针和引用的不同点:
1.寻址和defererence操作的显式还是隐式, 指针在使用的时候我们显示地 (explicitly) 获取了指向地址, 同时显示地 (explicitly) 通过 dereference 指针操作去获取值; 而引用的”寻址”和 deference 操作是隐式 (implicitly) 的
2.引用必须被初始化, 指针默认不被初始化 (但是作为developer应该以初始化作为必要原则)
3.引用不是 objects, 但是指针是 objects
4.引用不能修改, 指针可以任意修改指向的对象, 因此指针比引用更加灵活, 从另一个角度来说也更容易出错
5.引用永远需要绑定在一个 objects 上面, 但是指针可以指向 nothing
6.引用在使用上的通常安全性更强, 指针很灵活, 但是容易各种出问题
#include <iostream>
int main() {
int x{5};
int& ref {x}; // ref绑定了x
int* ptr {&x}; // ptr指向了x
std::cout << x;
std::cout << ref; // 直接使用ref来打印变量x
std::cout << *ptr << '\n'; // 直接使用*ptr打印变量;
ref = 6; // use the reference to change the value of x
std::cout << x;
std::cout << ref; // use the reference to print x's value (6)
std::cout << *ptr << '\n'; // use the pointer to print x's value (6)
*ptr = 7; // use the pointer to change the value of x
std::cout << x;
std::cout << ref; // use the reference to print x's value (7)
std::cout << *ptr << '\n'; // use the pointer to print x's value (7)
return 0;
}
打印的结果为
555
666
777
The size of pointers 指针的大小
1.指针的 size 取决于架构, 同时无视指向的类型, 不管是是什么类型, 大小总是相同的, 因为只是内存地址, 地址所需要保存的bits数量是一个固定值
#include <iostream>
int main() {
char* chPtr{}; // chars are 1 byte
int* iPtr{}; // ints are usually 4 bytes
long double* ldPtr{}; // long doubles are usually 8 or 12 bytes
std::cout << sizeof(chPtr) << '\n'; // prints 8
std::cout << sizeof(iPtr) << '\n'; // prints 8
std::cout << sizeof(ldPtr) << '\n'; // prints 8
return 0;
}
Dangling pointers 悬空指针
a dangling pointer is a pointer that is holding the address of an object that is no longer valid (e.g. because it has been destroyd)
Dereferencing a dangling pointer (e.g. in order to print the value being pointed at) will lead to undefined behavior, as you are trying to access an object that is no longer valid.
Perhaps surprisingly, the standard says “Any other use of an invalid pointer value has implementation-defined behavior”. This means that you can assign an invalid pointer a new value, such as nullptr (because this doesn’t use the invalid pointer’s value). However, any other operations that use the invalid pointer’s value (such as copying or incrementing an invalid pointer) will yield implementation-defined behavior.
intuitively,
1.dangling pointer (悬空指针) 就是说一个指针代表的地址上的 object 已经销毁了&无效了, 所以这个指针好比悬空的挂起来一样
2.对于一个悬空的指针, 会导致 undefined behavoir (未定义行为); 对一个 dangling pointer 采用 dereference 操作就是未定义行为
3.cpp 标准表示”无效指针值的任何其他使用都具有实现定义的行为”. 这意味着可以为无效指针分配一个新值, 例如 nullptr (因为这不使用无效指针的值)。 但是,使用无效指针值的任何其他操作(例如复制或递增无效指针)将产生实现定义的行为
#include <iostream>
int main() {
int x{5};
int* ptr{&x};
std::cout << *ptr << std::endl; // valid
{
int y{6};
ptr = &y;
std::cout << *ptr << std::endl; // valid
} // y goes out of scope, and ptr is now dangling
std::cout << *ptr << std::endl; // undefined behavior from dereferencing a dangling pointer
return 0;
}
我的输出是
5
6
6
Reference
1.https://www.learncpp.com/cpp-tutorial/introduction-to-pointers/.
转载请注明来源, from goldandrabbit.github.io