640K ought to be enough for everybody. ——Bill Gates
1.指针的概念
计算机体系中的存储层次
C++中内存单元内容与地址
- 内存由很多内存单元组成。这些内存单元用于存放各种类型的数据。
- 计算机对内存的每个内存单元都进行了
编号
,这个编号就称为内存地址
,地址决定了内存单元在内存中的位置。 - 记住这些内容单元地址不方便,于是C++语言的编译器让我们通过名字来访问这些内存位置。
举例:
int a = 112,b = -1;
float c = 3.14;
int* d = &a;
float* e = &c;
指针的定义和间接访问操作
- 指针定义的基本形式:指针本身就是一个变量,其符合变量定义的基本形式,它存储的是值的地址。对类型T,T*是"到T的指针"类型,一个类型为
T*的变量能保存一个类型T的对象的地址
。
如:
int a = 112; float c = 3.14;
int* d = &a; float e = &c;
注:定义float类型建议在变量后面加上f。
- 通过一个指针访问它所指向地址的过程称为间接访问或者引用指针;这个用于执行间接访问的操作符示单目操作符*;取地址符为
&
。
如:
cout<<(*d)<<endl;
cout<<(*e)<<endl;
总结
关于变量,地址和指针变量小结:
-
一个变量有三个重要信息:
A. 变量的地址位置;
B. 变量所存的信息;
C. 变量的类型; -
指针变量时一个专门用来记录变量的地址的变量;通过指针变量可以间接访问另一个变量的值(这个变量有可能是另一个指针变量)
2.左值与右值
数组与指针
int main()
{
//定义一个数组
char strHelloWorld[] = {"helloworld"};
char* pStrHelloWorld = "helloworld";
pStrHelloWrold = strHelloWorld; //指针变量的值允许改变
strHelloWorld = pStrHelloWrold; //数组变量的值不允许改变
}
StrHelloWorld不可变,strHelloWorld[index]的值可变;
pStrHelloWrold可变,pStrHelloWrold[index]的值可变不可变取决于所指区间的存储区域是否可变
;
左值和右值
- 概念:
一般说法,编译器为其单独分配了一块存储空间,可以取其地址的,左值
可以放在赋值运算符左边;右值
指的是数据本身;不能取到其自身地址,右值智能赋值运算右边; - 具体分析:
左值最常见的情况如函数和数据成员的名字;
右值是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。
比如:
a = b + c;
&a是允许的操作,而&(b+c)不能通过编译,因此a是一个左值,而(b+c)是一个右值。
3.一般指针、数组指针和指针数组
几种C++中的原始指针
- 一般类型指针T*
T是一个泛型,泛指任何一种类型;
如:
int i = 4; int* iP = &i;cout<<(*iP)<<endl;
double d = 4; double* dP = &d;cout<<(*dP)<<endl;
char c = 4; char* cP = &c;cout<<(*cP)<<endl;
注意:不同类型的指针存储空间基本一致,一般都是4字节16进制的整型数。
- 指针的数组(array of pointers)与数组的指针(a pointer to an array):
指针的数组T* t[]
数组的指针 T(*t) []
如:
int* a[4];
int (*b)[4]; //注意:[]优先级比较高;
举例说明:
int c[4] = {0x80000000,0xFFFFFFFF,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; //-2147483648
cout<<(*b)[3]<<endl; //2147483647
4.const与指针
- const pointer与pointer to const
例子:
char strHelloworld[] = {"helloworld"};
char const *pStr1 = "helloworld"; //存储空间内容不可变
const char *pStr1 = "helloworld"; //存储空间内容不可变
char* const pStr2 = "helloworld"; //指针不可变
char const * const pStr3 = "helloworld"; //存储空间内容和指针都不可变
pStr1 = strHelloworld;
//pStr2 = strHelloworld; //pStr2不可改
//pStr3 = strHelloworld; //pStr3不可改
- 关于const修饰部分:
1.看左侧最近的部分;
2.如果左侧没有,则看右侧;
5.指向指针的指针
- 指向指针的指针
例子:
int a = 123;
int* b = &a;
int** c = &b;
- *操作符具有从右向左的结合性
**这个表达式相当于*(*c),必须从里向外逐层求值;
*c得到的是c指向的位置,即b;
**c相当于*b,得到变量a的值;
表达式 | 表达式的值 |
---|---|
a | 123 |
b | &a |
*b | a,123 |
c | &b |
*c | b,&a |
**c | *b,a,123 |
6.关于野指针
- 未初始化和非法的指针
例子:
int *a;
和int* a;
两者没有区别。
int *a;
*a = 12;
注意:a这里指向哪里?这里会发生什么?
运气好的话:定位到一个非法地址,程序会出错,从而终止。
定位到一个可以访问的地址,无意修改了它,这样的错误难以捕捉,引发的错误可能与原先用于操作的代码完全不相干!
用指针进行间接访问之前,一定要非常小心,确保它已经初始化,并被恰当的赋值。
NULL指针
一个特殊的指针变量,表示不指向任何东西(避免了不确定性访问)。
如:
int *a = NULL;
NULL指针的概念非常有用:
它给了一种方法,来表示特定的指针目前未指向任何东西。
使用的注意事项:
对于一个指针,如果已经知道将被初始化为什么地址,那么请赋给它这个地址值,否则请把它设置为NULL
。
在对一个指针进行间接引用前,请先判断这个指针的值是否为NULL
。
int main()
{
//指针的指针
int a = 123;
int* b = &a;
int** c = &b;
//NULL的使用
int* pA = NULL;
pA = &a;
if(pA != NULL)//判断NULL指针
{
cout<<(*pA)<<endl;
}
pA = NULL;//pA不用时,置为NULL
return 0;
}
杜绝"野"指针
指向"垃圾"内存的指针。if等判断对它们不起作用,因为没有置NULL;
-
一般有三种情况:
1.指针变量没有初始化;
2.已经释放不用的指针没有置NULL,如delete和free之后的指针;
3.指针操作超越了变量的作用范围; -
指针使用的注意事项:
没有初始化的,不用的或者超过范围的指针请把值置为NULL
。
7.指针的基本操作
&与*操作符
char ch = 'a';char* cP = &ch;
int main()
{
char ch = 'a';
//&操作符
&ch = 97; //&ch左值不合法
char* cp = &ch; //&ch右值
//&cp = 97; //&cp左值不合法
char** cpp = &cp; //&cp右值
//*操作符
*cp = 'a'; //*cp左值取变量ch位置
char ch2 = *cp; //*cp左值取变量ch存储的值
//*cp + 1 = 'a'; //*cp+1左值不合法的位置
ch2 = *cp + 1; //*cp+1右值取到的字符做ASCII码+1操作
*(cp + 1) = 'a'; //*(cp+1)左值语法上合法,取ch后面位置的值
ch2 = *(cp + 1); //*(cp+1)右值语法上合法,取ch后面位置的值
return 0;
}
++与–操作符
//++,--操作符
char* cp2 = ++cp;
char* cp3 = cp++;
char* cp4 = --cp;
char* cp5 = cp--;
++操作符进一步说明
注意:++操作符的优先级高于取间接*操作符的优先级。
关于++++,----等运算符
编译器程序分解成符号的方法是:一个字符一个字符的读入,如果该字符可能组成一个符号,那么读入下一个字符,一直到读入的字符不再能组成一个有意义的符号。这个处理过程称为"贪心法"。
例如:
int a = 1,b = 2,c;
c = a+++b; //相当于a++ +b;
d = a++++b; //相当于a++ ++b,error
int main()
{
char ch = 'a';
char* cp = &ch;
//++,--操作符
char* cp2 = ++cp;
char* cp3 = cp++;
char* cp4 = --cp;
char* cp5 = cp--;
//++ 左值
//++cp2 = 97;
//cp2++ = 97;
//*++,++*
*++cp2 = 98;
char ch3 = *++cp2;
*cp2++ = 98;
char ch4 = *cp2++;
//++++,----操作符等
int a = 1,b = 2,c,d;
//c = a++b; //error
c = a++ + b;
//d = a++++b; //error
char ch5 = ++*++cp;
return 0;
}
8.CPP程序的存储区域划分
栈和队列
在C++中,一般变量都存储在栈中。
#include <cstddef>
#include <cstring>
using namespace std;
int a = 0; //(GVAR)全局初始化区
int* p1; //(bss)全局未初始化区
int main() //(text)代码区
{
int b = 1; //(stack)栈区变量
char s[] = "abc"; //(stack)栈区变量
int* p2 = NULL; //(stack)栈区变量
const 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)代码区
return 0; //(text)代码区
}
9.CPP程序的存储区域划分总结
代码和数据在C++程序中的存储
还有一个常量区,在heap和全局GVAR之间。
10.CPP动态分配和回收原则
堆heap
- 动态分配资源——堆(heap):
- 从现代的编程语言角度来看,使用堆,或者说使用动态内存分配,是一件很自然不过的事情。
- 动态内存带来了不确定性:内存分配耗时需要多久?失败了怎么办?在实时性要求比较高的场合,如一些嵌入式控制器和电信设备。
- 一般而言,当我们在堆上分配内存时,很多语言会使用new这样的关键字,有些语言则是隐式分配。在C++中new的对应词是delete,因为C++是可以让程序员完全接管内存的分配释放的。
分配和回收动态内存的原则
程序通常牵扯到三个内存管理器的操作:
- 分配一个某个大小的内存块;
- 释放一个之前分配的内存块;
- 垃圾收集操作,寻找不再使用的内存块并予以释放;这个回收策略需要实现性能、实时性、额外开销等各方面的平衡,很难有统一和高效的做法;
C++做了1,2两件事;而Java则做了1,3两件事;
11.RAII初步
资源管理方案——RAII
RAII(Resource Acquisition Is Initialization)
- C++所特有的资源管理方式。有少量其他语言,如D、Ada和Rust也采纳了RAII。
但主流的编程语言中,C++是唯一一个依赖RAII来做资源管理的。
- RAII依托栈和析构函数,来对所有的资源——包括堆内存在内进行管理。对RAII的使用,使得C++不需要类似于Java那样的垃圾收集方法,也能有效地对内存进行管理。RAII的存在,也是垃圾收集。虽然理论上可以在C++使用,但从来没有真正流行过的主要原因。
- RAII有些比较成熟的智能指针代表:如std::auto_ptr和boost::shared_ptr
12.几种变量的对比
栈和堆中的变量对比
栈(stack)区 | 堆(heap)区 | |
---|---|---|
作用域 | 函数体内,语句块{}作用域; | 整个程序范围内,由new,malloc开始,delete,free结束; |
编译间大小确定 | 变量大小范围确定 | 变量大小范围不确定,需要运行期确定; |
大小范围 | Windows系统默认栈大小是1M ,linux常见默认的栈大小是8M或10M (通过ulimit -s 查看;不同linux发行版的命令不保证相同) | 所有系统的堆空间上限是接近内存(虚拟内存)的总大小的(一部分被OS占用); |
内存分配方式 | 地址由高到低减少 | 地址由低到高增加 |
内存是否可变 | 可变 | 可变 |
全局静态存储区和变量存储区的变量对比
全局静态存储区 | 常量存储区 | |
---|---|---|
存储内容 | 全局变量,静态变量 | 常量 |
编译期间大小是否确定 | 确定 | 确定 |
内容是否可变 | 可变 | 不可变 |
#include <cstddef>
#include <cstring>
using namespace std;
int a = 0; //(GVAR)全局初始化区
int* p1; //(bss)全局未初始化区
int main() //(text)代码区
{
int b = 1; //(stack)栈区变量
char s[] = "abc"; //(stack)栈区变量
int* p2 = NULL; //(stack)栈区变量
const 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;
}
return 0; //(text)代码区
}
注意:需要注意的是,不要混用delete(C++)和free ( C)。
13.内存泄漏
内存泄漏(Memory Leak)问题
- 什么是内存泄漏问题:
指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 - 内存泄漏发生原因和排查方式:
- 内存泄漏主要发生在堆内存分配方式中,即"配置了内存后,所有指向该内存的指针都遗失了"。若缺乏语言这样的垃圾回收机制,这样的内存片就无法归还系统。
- 因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判断和诊断。
14.智能指针auto_ptr
比指针更安全的解决方案
使用指针是非常危险的行为,可能存在空指针。野指针问题,并可能造成内存泄漏问题。
可指针又非常的高效,所以我们希望以更安全的方式来使用指针。
- 一般有两种典型的方案:
- 使用更安全的指针——智能指针;
- 不使用指针,使用更安全的方式——引用;
C++的智能指针
- C++中推出了四种常用的智能指针:
- unique_ptr、shared_ptr、weak_ptr和C++ 11中已经废弃(deprecated)的auto_ptr,在C++ 17中被正式删除;
- 这里,我们从应用方面来分析这几种智能指针:
- 应用场景:
A. 对象所有权;
B. 生命周期; - 性能分析;
auto_ptr
头文件:#include<memory>
有new_expression获得对象,在auto_ptr对象销毁时,它所管理的对象也会自动被delete掉。
所有权转移:不小心把它传递给另外的智能指针,原来的指针就不再拥有这个对象了。在拷贝/赋值过程中,会直接剥夺指针对原对象对内存的控制权,转交给新对象,然后再将原对象指针置为nullptr。
#include <string>
#include <iostream>
#include <memory>
using namespace std;
int main()
{
{// 确定auto_ptr失效的范围
// 对int使用
auto_ptr<int> pI(new int(10));
cout << *pI << endl; // 10
// auto_ptr C++ 17中移除 拥有严格对象所有权语义的智能指针
// auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,
// 然后再将原对象指针置为nullptr(早期:NULL)。这种做法也叫管理权转移。
// 他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以auto_ptr可以说实现的不好,
// 很多企业在其库内也是要求不准使用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("Rust"))
};
cout << "There are some computer languages here first time: \n";
for (int i = 0; i < 5; ++i)
{
cout << *languages[i] << endl;
}
auto_ptr<string> pC;
pC = languages[2]; // languges[2] loses ownership. 将所有权从languges[2]转让给pC,
//此时languges[2]不再引用该字符串从而变成空指针
cout << "There are some computer languages here second time: \n";
for (int i = 0; i < 2; ++i)
{
cout << *languages[i] << endl;
}
cout << "The winner is " << *pC << endl;
//cout << "There are some computer languages here third time: \n";
//for (int i = 0; i < 5; ++i)
//{
// cout << *languages[i] << endl;
//}
}
return 0;
}
注意:使用nullptr代替NULL对指针判断是否为空。
15.智能指针unique_ptr
unique_prt
unique_prt是专属所有权,所以unique_ptr管理的内存,只能被一个对象持有,不支持复制和复赋值。
移动语义:unique_ptr禁止了拷贝语义,但有时我们也需要能够转移所有权,于是提供了移动语义,即可以使用std::move()
进行控制所有权的转移。
#include <memory>
#include <iostream>
using namespace std;
int main()
{
// 在这个范围之外,unique_ptr被释放
{
auto i = unique_ptr<int>(new int(10));
cout << *i << endl;
}
// unique_ptr
auto w = std::make_unique<int>(10);
cout << *(w.get()) << endl; // 10
//auto w2 = w; // 编译错误如果想要把 w 复制给 w2, 是不可以的。
// 因为复制从语义上来说,两个对象将共享同一块内存。
// unique_ptr 只支持移动语义, 即如下
auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl; // -1
cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl; // 10
return 0;
}
16.shared_ptr和weak_ptr理论讲解
shared_ptr
shared_ptr通过一个引用计数共享一个对象。
shared_ptr是为了解决auto_ptr在对象所有权上的局限性,在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销。
当引用计数为0时,该对象没有被使用,可以进行析构。
- 循环引用:引用计数会带来循环引用的问题
循环引用会导致堆里的内存无法正常回收,造成内存泄漏。
weak_ptr
weak_ptr被设计为与shared_ptr共同工作,用一个观察者模式工作。
作用是协助shared_ptr工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。
观察者意味着weak_ptr只对shared_ptr进行引用,而不改变其引用计数,
当被观察的shared_ptr失效后,相应的weak_ptr也相应失效。
17.shared_ptr和weak_ptr代码演示
#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
// cout << wA.use_count() << endl; // 2
// }
// //cout << wA2.use_count() << endl;
// cout << wA.use_count() << endl; // 1
// cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl; // 20
// //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
cout << wAA2.use_count() << endl; // 1
//将 wAA 对象 move 给 wAA2,意味着 wAA 放弃了对内存的所有权和管理,此时 wAA对象等于 nullptr。
//而 wAA2 获得了对象所有权,但因为此时 wAA 已不再持有对象,因此 wAA2 的引用计数为 1。
return 0;
}
#include "stdafx.h"
#include <string>
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> pb;
~A()
{
cout << "~A()" << endl;
}
};
struct B {
shared_ptr<A> pa;
~B()
{
cout << "~B()" << endl;
}
};
// pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
// weak_ptr 是为了解决 shared_ptr 双向引用的问题。
struct BW;
struct AW
{
shared_ptr<BW> pb;
~AW()
{
cout << "~AW()" << endl;
}
};
struct BW
{
weak_ptr<AW> pa;
~BW()
{
cout << "~BW()" << endl;
}
};
void Test()
{
cout << "Test shared_ptr and shared_ptr: " << endl;
shared_ptr<A> tA(new A()); // 1
shared_ptr<B> tB(new B()); // 1
cout << tA.use_count() << endl;
cout << tB.use_count() << endl;
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; // 2
cout << tB.use_count() << endl; // 2
}
void Test2()
{
cout << "Test weak_ptr and shared_ptr: " << endl;
shared_ptr<AW> tA(new AW());
shared_ptr<BW> tB(new BW());
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 1
tA->pb = tB;
tB->pa = tA;
cout << tA.use_count() << endl; // 1
cout << tB.use_count() << endl; // 2
}
int main()
{
Test();
Test2();
return 0;
}
18.引用
C++的引用
- 引用是什么?
是一种特殊的指针,不允许修改的指针。
使用指针有哪些坑:
- 空指针
- 野指针
- 不知不觉改变了指针的值,却继续使用;
使用引用,则可以:
4. 不存在空引用;
5. 必须初始化;
6. 一个引用永远指向它初始化的那个对象;
- 引用的基本使用:可以认为是指定变量的别名,使用时可以认为是变量本身;
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
- 有了指针为什么还需要引用?
Bjarne Stroustrup(C++之父)的解释:
为了支持函数运算符重载
; - 有了引用为什么还需要指针?
Bjarne Stroustrup(C++之父)的解释:
为了兼容C语言;
注意:C语言完全用指针,JAVA语言完全用引用。
补充
关于函数传递参数类型的说明:
- 对内置基础类型(如int,double等)而言,在函数中传递pass by value更高效;
- 对OO面向对象中自定义类型而言,在函数中传递时pass by reference to const更高效