5. 指针

1. 指针的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# include <iostream>
using namespace std;

int main(){
int a = 112, b = 1;
float c = 3.14f;
int* d = &a;
float* e = &c;
cout << d << endl;
cout << e << endl;
cout << (*d )<< endl;
cout << (*e) << endl;

return 0;
}

OUT:
0xa1fe04
0xa1fe00
112
3.14

2. 左值和右值

一般说法,编译器为其单独分配了一块存储空间,可以取其地址的,左值可以放在赋值运算符左边;

右值是数据本身,不能取到其自身地址,右值只能赋值运算右边。

具体来说:

  • 左值最常见的情况如函数和数据成员的名字;
  • 右值是没有标识符、不可以取地址的表达式,一般称之为“临时对象”
  • &a是允许的,而&(b+c)不能通过编译,因此a是一个左值,(b+c)是一个右值

3. 指针的数组、数组的指针

  • 指针的数组 array of pointers : T* t[]
  • 数组的指针 a pointer to an array : T (*t) [] []优先级比较高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int main(){
int i = 4;
int* iP = &i;
cout << (*iP) << endl;
double d = 3.14;
double * dP = &d;
cout << (*dP) << endl;
char c = 'a';
char * cP = &c;
cout << (*cP) << endl;

}

-OUT:
4
3.14
a


int main(){

// array of pointers 和 a pointer to an array
int c[4] = {0x40000000, 0x4FFFFFFF, 0x00000000, 0x7FFFFFFF};
int* a[4]; //array of pointers
int(*b) [4]; // a pointer to an array
b = &c; //这里数组个数得匹配,
// 将数组c中元素赋给数组a
for (unsigned int i = 0; i<4; i++)
{
a[i] = &(c[i]);
}
cout << *(a[0]) << endl;
cout << *(b) [3] << endl;
return 0;
}

-OUT:
1073741824
2147483647

4. const pointer 与 pointer to const

1
2
3
4
5
6
7
8
9
10
11
12
char  strHelloworld[] = {"helloworld"}; 
char const *pStr1 = "helloworld";
// 修饰char 说明内容是不可改变的
const char *pStr1 = "helloworld";
// 修更常见的写法
char* const pStr2 = "helloworld";
// 修饰指针, 说明不变的是地址
char const * const pStr3 = "helloworld";
//两个const分别修饰char和指针,说明内容和地址都不可改变
pStr1 = strHelloworld;
//pStr2 = strHelloworld; //pStr2不可改
//pStr3 = strHelloworld; //pStr3不可改
  • 关于const修饰部分:
    1. 看左侧最近的部分
    2. 如果左侧没有,则看右侧

5. 指针的指针

1
2
3
int a = 12;
int* b = &a;
int** c = &b;
  • *操作符具有从右向左的结合性, **相当于*(*c),必须从里向外逐层求值,*c是c指向的位置,即b;**c相当于 *b,得到变量a的值。

6. NULL

int* a = NULL

  • 一个特殊的指针变量,表示不指向任何东西
  • 它给了一种方法,来表示特定的指针目前未指向任何东西
  • 使用注意事项:
    • 对于一个指针,如果已经知道江北初始化为什么样的地址,那么请赋给它这个地址值,否则把它设为NULL
    • 在对一个指针进行间接引用前,请先判断这个指针的值是否为NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main() {
int a = 123;
int *b = &a;
int **c = &b;
// NULL 的使用
int *pA = NULL;
pA = &a;
if (pA != NULL){
cout << (*pA) << endl;
}
pA = NULL; // 不用时一定又置为NULL
return 0;
}
OUT:
123

杜绝“ 野 ”指针

  • 指向“垃圾”内存的指针。if判断对其不起作用,因为不为NULL。

一般有三种情况:

  1. 指针变量没有初始化
  2. 已经释放不用的指针没有置NULL,如delete和free之后的指针
  3. 指针操作超越了变量的作用范围

Note: 没有初始化的,不用的或者超出范围的指针请把值置为NULL。

7. 指针相关操作符

& 与 * 的比较:

左值取地址,右值取地址里的内容。

左值和右值??

8. ++ 与 - - 操作符

  • ++ i : 先++, 再赋值给i
  • i++: 先赋值给i, 再++

++i和i++区别???

1
2
3
int a = 1; b = 2; c;
c = a+++b; // 相当于a++ +b
d = a++++b; // 相当于a++ ++b, error

9. 内存分析

1.栈和队列

  1. 栈区(stack)——由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

  2. 堆区(heap)—— 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

  3. 全局区(静态区)(static)——,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
  4. 文字常量区 ——常量字符串就是放在这里的。 程序结束后由系统释放
  5. 程序代码区——存放函数体的二进制代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <string>

int a = 0; //(GVAR)全局初始化区
int* p1; //(bss)全局未初始化区
int main() //(text)代码区
{
int b=1; //(stack)栈区变量
char s[] = "abc"; //(stack)栈区变量
int*p2=NULL; //(stack)栈区变量
char *p3 = "123456"; //123456\0在常量区, p3在(stack)栈区
static int c = 0; //(GVAR)全局(静态)初始化区
p1 = new int(10); //(heap)堆区变量
p2 = new int(20); //(heap)堆区变量
char* p4 = new char[7]; //(heap)堆区变量
strcpy_s(p4, 7, "123456"); //(text)代码区

//(text)代码区
if (p1 != NULL)
{
delete p1;
p1 = NULL;
}
if (p2 != NULL)
{
delete p2;
p2 = NULL;
}
if (p4 != NULL)
{
delete[ ] p4;
p4 = NULL;
}
//(text)代码区
return 0; //(text)代码区
}

cppstorage

堆heap——动态分配资源

  1. 从现代语言的编程角度看,使用堆,或者说使用内存分配是很自然的;
  2. 动态内存带来了不确定性:内存分配耗时要多久?失败了怎么办?在实时性比较高的场合,如嵌入式控制器和电信设备。
  3. 一般地,当我们在堆上分配内存时,很多语言使用new这样的关键字,有些语言则是隐式分配。在c++中new的对应词是delete,因为c++是可以让程序员完全结果内存的分配释放的。

2. 资源管理方案——RAII

Resource Acquisition Is Initialization:

  • C++所特有的资源管理方式。主流语言中,C++是唯一一个依赖RAII做资源管理的
  • RAII依托栈和析构函数,来对所有资源——包括堆内存在内进行管理。
  • RAII有些比较成熟的智能指针代表:std::auto_ptrboost::shared_ptr

3. C++ 中栈和堆的对比

stack heap
作用域 函数体内,语句块{}作用域; 整个程序范围内,由new,malloc开始,delete, free结束
编译期间大小确定 变量大小范围确定 变量大小范围不确定,需要运行期确定
大小范围 win:1M,linux默认8M或10M,可以用ulimit -s查询 所有系统的堆空间大小是接近内存(虚拟内存)的总大小的(一部分被OS占用)
内存分配方式 地址由高到低减少 地址由低到高增加
内容是否可变 可变 可变

4. 内存泄漏

定义

​ 程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

原因和排查方式

  1. 内存泄漏主要发生在堆内存分配方式中,即“配置内存后,所有指向该内存的指针都遗失了”。若缺乏语言的垃圾回收机制,这样的内存就无法归还系统。
  2. 因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判别和诊断。

10. C++的智能指针

C++中推出了四种常用的智能指针:

  • unique_ptr
  • shared_ptr
  • weak_ptr: C++ 11中已经被遗弃
  • auto_ptr:C++ 17中正式删除。

从应用方面来分析这几种智能指针:

  1. 应用场景
    • 对象所有权
    • 生命周期
  2. 性能分析

1. auto_ptr

由new expression 获得对象,在auto_ptr 对象销毁是,其所管理的对象也会被自动delete掉。

所有权转移:不小心把它传递给另外的智能指针,原来的指针就不在拥有这个对象了。转交给新对象,然后再将原对象指针置为nullptr。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <string>
#include <iostream>
#include <memory>

using namespace std;

int main(){
{// 这个大括号只是为了确定auto_ptr作用范围 int
auto_ptr<int> pl(new int(10));
cout << *pl << endl;

// auto_ptr C++ 17中移除 拥有严格对象所有权语义的智能指针
// auto_ptr 原理: 在拷贝和赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象
// 然后再将原对象指针置为nullptr,这种做法也叫管理权转移
// 缺点:当我们再次去访问原对象是,程序就会报错,所以auto_ptr实现原理就不好
auto_ptr<string> languages[5] ={
auto_ptr<string>(new string("C")),
auto_ptr<string>(new string("Java")),
auto_ptr<string>(new string("C++")),
auto_ptr<string>(new string("Python")),
auto_ptr<string>(new string("GO"))
};
cout << "There are some computer languagues here: \n";
for (int i=0; i < 2; ++i){
cout << *languages[i] <<endl;
}
auto_ptr<string> pC;
pC = languages[2];//languages[2] 失去所有权,将languages[2]转交给pC
// 此时languages[2]不再引用该字符串从而变成空指针

cout << "There are some computer languagues here: \n";
for (int i=0; i < 2; ++i){
cout << *languages[i] <<endl;
}
cout << "The winner is " << *pC << endl;
}
return 0;
}

——————————————————————————————
10
There are some computer languagues here:
C
Java
There are some computer languagues here:
C
Java
The winner is C++

2. unique_ptr

unique_ptr:是专属所有权,所以unique_ptr管理的内存,只能被一个对象持有,不支持复制和赋值。

移动语义:unique_ptr禁止了拷贝语义,但有时我们也需要能够转移所有权,用std::move()进行控制所有权的转移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>    
#include <memory>
using namespace std;

int main(){
auto w = std::make_unique<int>(10);
cout << *(w.get()) << endl;

//auto w2 = w;//编译错误,无法讲w复制给w2
//因为复制从语义上来说,两个对象将共享同一块内存

// unique_ptr 只支持移动语义
auto w2 = std::move(w); // w2获得内存所有权,w此时等于nullptr
cout << ((w.get() != nullptr)? (*w.get()):-1) << endl;
cout << ((w2.get() != nullptr)?(*w2.get()):-1) << endl;


return 0;
}
——————————————————————————————————————
10
-1
10

cout << ((w.get() != nullptr)? (*w.get()):-1) << endl 三目运算符, (w.get() != nullptr)?成立吗?成立取:前面,否则取:后面。

3. shared_ptr

shared_ptr通过一个引用计数共享一个对象。

shared_ptr是为了解决auto_ptr在对象所有权上的局限性,在使用引用计数的机制上提供了可以共享所有权的智能指针,当然也需要额外的开销。

当引用计数为0时,该对象没有被使用,可以进行析构

4. weak_ptr

weak_ptr被设计为与 shared_ptr共同工作,用一种观察这模式工作。

作用:协作 shared_ptr工作,可获得资源的观测权,像旁观者那样观测资源使用情况。

意味着 weak_ptr只对 shared_ptr进行引用,而不改变其引用计数,当被观察的shared_ptr失效后,相应的 weak_ptr也相应失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>    
#include <memory>
using namespace std;
int main(){
//shared_ptr
{
// shared_ptr 代表的是共享所有权, 即多个shared_ptr 可以共享同一块内存
auto wA = shared_ptr<int>(new int(20));
{
auto wA2 = wA;
cout << ((wA2.get() != nullptr)? (*wA2.get()):-1) << endl; //20
cout << ((wA.get() != nullptr)? (*wA.get()):-1) << endl;//20
cout << wA2.use_count() << endl; //2 wA2.use_count() 引用计数
cout << wA.use_count() << endl; //2
}
//cout << wA2.use_count() << endl; //
cout << wA.use_count() << endl; // 1
// shared_ptr内部是利用计数来实现内存的自动管理,每当赋值一个shared_ptr
// 引用计数会+1.当shared_ptr离开作用域时,引用计数会-1
// 当引用计数为0的时候,则delete内存
}
// move 语法
auto wAA = std::make_shared<int>(30);
auto wAA2 = std::move(wAA); // 此时wAA等于nullptr, wAA2.use_count()等于1
cout << ((wAA.get() != nullptr)? (*wAA.get()):-1) << endl; //-1
cout << ((wAA2.get() != nullptr)? (*wAA2.get()):-1) << endl;//30
cout << wAA.use_count() << endl; //0 wA2.use_count() 引用计数
cout << wAA2.use_count() << endl; //1
// 将wAA 对象move给wAA2,意味着wAA放弃了对内存的所有权和管理,此时wAA对象等于nullptr
// 而wAA2获得了对象所有权,但因为此时wAA已不再持有对象,因此wAA2引用计数减1
return 0;
}
----------------------------------------
20
20
2
2
1
-1
30
0
1

11. C++的引用

1.引用

引用: 是一种特殊的指针,不允许修改的指针。

  • 使用指针的坑:

    • 空指针
    • 野指针
    • 不知不觉改变了指针的值,却继续引用
  • 引用:

    • 不存在空引用
    • 必须初始化
    • 一个引用永远指向它初始化的对象

2. 基本使用

引用:可以认为是指定变量的别名,使用是可以认为是变量本身。

int x = 1;

int &rx = x;这两句中&是引用,注意与指针的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <assert.h>
using namespace std;

void swap(int &a, int &b){
int temp;
temp = a;
a = b;
b = temp;

}
void swap2(int *a, int *b){
int temp;
temp = *a;
*a = *b;
*b = temp;

}

int main(){
int x = 1, x2 = 3;
int &rx = x;
rx = 2;
cout << x << endl; // 2
cout << rx << endl;//2
rx = x2;
cout << x << endl;//3
cout << rx << endl;//3

int a = 3, b = 4;
swap(a, b);
assert(a==4&&b==3);
swap2(&a, &b);
assert(a==3&&b==4);
swap(rx, x);
cout << x << endl;//3
cout << rx << endl;//3
rx = x2 + 1;
swap(rx, x);
cout << x << endl;//4
cout << rx << endl;//4
}

3. C++的引用的作用

  • 有了指针为什么还要引用?——为了支持运算符重载;
  • 有了引用为什么还需要指针?——为了兼容c语言