C++初学者指南-3.自定义类型(第一部分)-指针
文章目录
- C++初学者指南-3.自定义类型(第一部分)-指针
- 1.为什么我们需要它们?
- 2.T 类型的对象指针
- 原始指针:T *
- 智能指针(C++11)
- 3.操作符
- 地址操作符 &
- 解引用运算符 *
- 成员访问操作符 ->
- 语法
- 重定向
- 4.nullptr (C++11)
- 5.const 和指针
- 6. "this"指针
- 7.前置类型声明
- 8. 尽量避免使用指针
1.为什么我们需要它们?
观察对象
- 不复制的间接引用:引用/跟踪对象
- 如果我们想要在运行时更改间接目标 ⇒ 不能使用引用
访问动态内存
- 访问具有动态存储期的对象,即生命周期不与变量或作用域绑定的对象(后续章节会介绍)。
构建动态的、基于节点的数据结构
2.T 类型的对象指针
- 存储 T 类型对象的内存地址
- 可用于检查/观察/修改目标对象
- 可以重定向到不同的目标(与引用不同)
- 也可能指向根本没有的对象(是 Null 指针)
原始指针:T *
- 本质上是一个(无符号的)整数变量,用于存储内存地址
- 大小:64 位平台上为64 位
- 许多原始指针可以指向相同的地址/对象
- 指针和目标(被指向的)对象的生命周期是独立的
智能指针(C++11)
std::unique_pointer
- 用于访问动态存储,即堆上的对象
- 每个对象只能有一个 unique_ptr
- 指针和目标对象具有相同的生命周期
std::shared_pointer
std::weak_pointer
- 用于访问动态存储,即堆上的对象
- 每个对象可以有多个shared_ptrs 或 weak_ptrs
- 只要至少有一个shared_ptr指向目标对象,目标对象就会存在
我们将在后面的章节中学习如何使用这些智能指针。
3.操作符
地址操作符 &
char c = 65;
char* p = &c;
- T* 类型的原始指针变量可以存储 T 类型对象的地址。
- &c 返回 C 的内存地址
解引用运算符 *
char c = 65;
char* p = &c;
*p = 88;
char x = *p;
- &c 返回 c 的内存地址
- *p 访问 p 中地址的值
成员访问操作符 ->
struct Coord {
char x = 0;
char y = 0;
};
Coord a {12,34};
Coord* p = &a;
char v = p->x; // v = 12
char w = p->y; // w = 34
// 另外的方式:
char s = (*p).x; // s = 12
char t = (*p).y; // t = 34
语法
* | & | |
---|---|---|
类型修饰符 | 指针声明 | 引用声明 |
一元运算符 | 解引用 value = *pointer; | 取得地址 pointer = &variable; |
二元运算符 | 乘法 product = expr1 * expr2; | 按位与 bitand = expr1 & expr2; |
声明陷阱
int* p1, p2; // int*, int
int *p1, *p2; // int*, int*
更好且更明确:
int* p1 = …;
int* p2 = …;
重定向
与引用不同,指针可以重定向
int a = 0;
int b = 0; // a: 0 b: 0
int* p = &a; // p→a a: 0 b: 0
*p = 2; // p→a a: 2 b: 0
p = &b; // p→b a: 2 b: 0
*p = 9; // p→b a: 2 b: 9
cout << a; // 2
cout << b; // 9
运行上面代码
4.nullptr (C++11)
- 特殊指针值
- 可以隐式转换为 false
- 在内存中不一定用 0 表示! (取决于平台)
编码约定:nullptr 表示值不可用
- 在初始化时将指针设置为 nullptr 或有效地址
- 在解引用之前检查是否不是 nullptr
int* p = nullptr; // 初始化为nullptr
if (…) {
int i = 5;
p = &i; // 分配有效地址
…
// 在解引用之前检查!
if (p) *p = 7;
…
// 设置为nullptr,表示“不可用”。
p = nullptr;
}
// i的内存被释放,任何指向i的指针都会变得无效!
5.const 和指针
目的
- 只读访问对象
- 防止指针重定向
语法
指向类型 T 的指针 | 指向值可修改 | 指针本身可修改 |
---|---|---|
T * | 可以 | 可以 |
T const * | 不可以 | 可以 |
T * const | 可以 | 不可以 |
T const * const | 不可以 | 不可以 |
从右到左读:“(const)指向(const)T的指针”
例子:
int i = 5;
int j = 8;
int const* cp = &i;
*cp = 8; // 编译器错误:指向的值是const
cp = &j; // OK
int *const pc = &i;
*pc = 8; // OK
pc = &j; // 编译器错误:指针本身是常量
int const*const cpc = &i;
*cpc = 8; // 编译器错误:指向的值是常量
cpc = &j; // 编译器错误:指针本身是常量
一个关于风格的持续辩论…
右const | 左const |
---|---|
一个一贯的规则: const 的剩余部分保持不变 | 更普遍,但不太一致 |
int const c = …; int const& cr = …; int const* pc = …; int *const cp = …; int const * const cpc = …; | const int c = 1; const int& cr = …; const int* pc = …; int *const cp = …; const int *const cpc = …; |
6. "this"指针
- 成员函数内部可用
- this 返回对象本身的地址
- this-> 可用于访问成员
- *this 访问对象本身
class IntRange {
int l_ = 0;
int r_ = 0;
public:
explicit
IntRange (int l, int r): l_{l}, r_{r} {
if (l_ > r_) std::swap(l_, r_);
}
int left () const { return l_; }
// can also use 'this' to access members:
int right () const { return this->r_; }
…
// returns reference to object itself
IntRange& shift (int by) {
l_ += by;
r_ += by;
return *this;
}
IntRange& widen (int by) {
l_ -= by;
r_ += by;
return *this;
}
};
运行上面代码
IntRange r1 {1,3}; // 1 3
r1.shift(1); // 2 4
r1.shift(2).widen(1); // 3 7
7.前置类型声明
有时候如果需要让两种类型相互引用的话是必要的:
// 前置声明
class Hub;
class Device {
Hub* hub_;
…
};
class Hub {
std::vector<Device const*> devs_;
…
};
为了定义一个类型,必须要知道它所有成员的内存大小。
- 这只有在完全了解所有成员的定义的情况下才可能实现。
- 但是,所有指针类型都具有相同的大小
⇒我们可以:
声明 Hub 的存在,因为 Device 只需要一个指向它的指针。
8. 尽量避免使用指针
指针容易悬空
- 悬空 = 指向无效/无法访问的内存地址的指针
- 存储在指针中的值可以是任何地址
- 程序员必须确保指针目标有效/仍然存在
int* p; // p 没有初始化!
*p = 7; // 未知行为
p = nullptr;
*p = 7; // 访问空指针导致未知行为
{
int x = 8;
p = &x;
} // x的生命周期已经结束
*p = 7; // 访问已经释放的内存导致未知行为
容易出错的参数传递
void swap_values (int* a, int* b) {
int t = *a;
*a = *b;
*b = t;
}
int x = 3, y = 4;
swap_values(&x, &y) // OK
swap_values(&x, 0); // 未知行为
swap_values(&x, nullptr); // 未知行为
代码更难阅读
*p = *p * *p + (2 * *p + 1); // 太多星号了!
建议:如果可能,首选引用,尤其是对于函数参数
附上原文地址
如果文章对您有用,请随手点个赞,谢谢!^_^