引用
引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。其格式为:类型 &引用变量名 = 已定义过的变量名。
引用特点
- 一个变量可取多个别名。
- 引用必须初始化。(指针可以任意时候赋值)
- 引用只能在初始化的时候引用一次 ,不能更改为转而引用其他变量。(指针可以指向其它变量)
- 引用在实现时被编译为const指针;
- 不能建立引用数组;
指针
指针存储的是变量的地址,而不是像普通变量那样存储的值。再python,java中没有指针类型。C++中还有一个与指针功能非常相似的操作:引用!
定义指针采用运算符作为标识,在计算过程中仍旧采用进行“解除引用”,即获取指针所指向地址的值。
事实上,对指针指向地址的值进行修改即是对原变量进行修改。// 定义指针
int number = 10;
int* pointer = &number; //其中&就是引用运算符,但是此处的意思时取地址,pointer的值等于number的地址
// 利用*进行解除引用操作
cout << "number is " << *pointer << endl;
// 利用指针修改number
*pointer = *pointer + 1
// 定义引用
int& ref = number
// 利用引用修改number
ref = number + 1
指针定义
格式:类型 p_name;其中两边的空格可选,通常情况下是*前面不留空格,后面留有空格。指针之间可以互相赋值。指针的危险
定义指针的时候,计算机将分配用来储存地址的空间,但不会分配用来存储指针所指向数据的内存。那么当指针没有被初始化的时候(指针存储的地址为随机值),在后续操作指针将会进行一些了不可控修改。
所以在定义指针的时候,将指针初始化一个确定的、适当的地址很重要!(可以给指针赋值为0,即null,指向空指针)指针和数字
指针存储的是地址,地址通常被当作整型来处理,但是地址和整数确实两个截然不同的类型。地址不能进行乘除操作等,也不能够地址加地址。
解决上面的问题,可以进行强制类型转换:int * pt;
pt = 0xB8000000; // 类型不匹配,会报错
但是指针可以进行偏移操作,每次偏移1,相当于地址增减和指针类型匹配的一个单位。(例如:int型指针+1操作和double型指针+1操作,一个将会在地址数值上加4,一个会加8)int * pt;
pt = (int *)0xB8000000;
由于地址的偏移操作,指针的一种用法和数组非常类似:
可以看到数组和指针具有极大的相似性!int a[10] = {1,2,3};
int * p = a; // a等于a[0]的地址,即数组名和数组首地址对应
// 以下将输出相同的结果
cout << "a[2] is " << a[2] << endl;
cout << "*(p+2) is " << *(p+2) << endl;
cout << "p[2] is " << p[2] << endl;
使用new进行动态分配
格式:typeName * pointer_name = new typeName;
这是指针最具有价值的地方之一!为已经被分配内存的变量分配指针只是为该变量赋予一个别名(此时相当于引用),利用new进行未命名空间分配时才能体现指针的价值,此时对该内存的访问只能通过指针实现!(C语言中可以通过malloc()库函数实现,但是比较麻烦)
值得注意的时,通常变量被储存在栈中(stack),但是new从堆(heap)或自由存储区的内存区域分配内存。char* cp = new char;
int * int_p = new int;
int* arr_pointer = new int[10];
使用delete释放内存
释放内存会释放指针指向的区域,但是不会删除指针本身。delete之后通常需要将指针指向null,防止后续发生问题。并且要注意new和delete配对使用,否则容易发生内存泄漏(内存泄漏)
不要尝试释放已经被释放的内存块,这会导致不确定的结果;并且不能用delete来释放不是通过new分配的内存(变量定义的等)!对空指针进行delete是安全的,所以delete之后可以将指针指向null来预防后续错误!
注意事项:int * ps = new int;
// 释放内存
delete ps;
- delete只能释放通过new分配的空间;
- 不是通过new分配的空间不能释放;
new分配时带[]则,delete时同样应该带[],new分配是没有带[],则delete是也不要带[];
动态数组的创建
利用new创建数组又叫做“动态联编”,对于new创建的数组同样可通过delete进行释放。
其中如果不用”[]”时并不会释放整个数组,只会释放ps指向的元素占用的内存!// 创建数组
int * ps = new int[100]
//内存释放
delete [] ps; // 方括号告诉程序释放整个数组,而不仅仅时指针指向的元素
一个例子
#include<iostream>
int main(){
using namespace std;
double * p3 = new double[3];
p3[0] = 0.1;
p3[1] = 0.2;
p3[2] = 0.3;
if (*(p3+1) == p3[1]){
cout << "OK!" << endl;
}
p3 = p3 + 1;
if (*p3 == 0.2){
cout << "OK!" << endl;
}
}
指针、数组和指针算术
指针和数组基本等价。指针变量加1,则指针的值增加量等于它所指向类型的字节数。数组名被解释为数组的首地址。
int stacks[3] = {1, 2, 3};
int * sp = stacks;
cout << *(stacks + 1) << endl; // stacks[1] == *(stacks + 1)
cout << *(sp + 1) << endl;
上面的例子说明,”[…]”无论是对数组还是指针来说都是取偏移的操作,数组名完全可以被当做是一个const的指针。
值得注意的是:数组名相对于数组的首地址,但是对数组名进行&操作将得到的是整个数组的地址(而非首地址),例如:int tell[10];
cout << tell << endl;
cout << &tell << endl; // 输出结果不同
其中的tell可以看做是一个int型的指针,但是&tell则被看做是size是int 10倍的数据类型的指针,即:
int (*pas) [10] = &tell // 或者:int (*)[10]类型;
此时pas和tell等价,(pas)[1]为tell数组第一个元素。
指针和字符串
看cout
char flower[10] = "rose";
cout << flower << "s are red?\n";
其中的flower是数组名,同样也是char数组的首地址,那么cout接受char数组的首地址之后,将会从该字符打印,知道碰到空字符为止;由于指针本身就是保存的地址,所以将char指针传入cout同样会得到同样的效果!与字符数组对应
"s are red?\n"
同样表示是一个字符数组,它同样表示的是一个地址!
由于cout的这种性质,如果想要打印地址,则需要进行强制类型转换:(int *)
:char flower[10] = "rose";
cout << (int *)flower; // 将会是16进制打印输出
注:也可以用
(int)
进行强制类型转换,两者的差别:前者为16进行输出,后者为10进制输出。字符串副本
值得注意的是,数组虽然和指针很类似,但是也存在诸多不同。比如,指针可以指向不同的地方,但是数组名虽然也是保存的一个地址,其地址会在申明时分配,并且不能改变 —> 数组名更像是一个const类型的指针。
生成数组的副本时,不能采用以下方式:char mychars[10] = "hello!";
char * cp = mychars; // 只是地址赋值,cp和mychars保存的同一个地址
正确做法:1、定义指针并新分配空间;2、字符串复制;
char mychars[10] = "hello!";
char * cp = new char[strlen(mychars + 1)];
strcpy(cp, mychars)
注:
strcpy存在一个问题:如果cp的空间不足以容纳mychars,则cp后面的内存会被超过cp空间的字符覆盖掉!(解决方法,利用strncpy中的n进行限制,n的大小包括了
"\0"
)当然,strlen和strcpy都是C-style的代码,后面C++风格的代码,将会运用运算符重载的方式解决这个问题;
使用new创建动态结构
结构和类非常相似,很多对于结构相关的技术也同样适用于类。
方法:创建结构;(
new
)- 访问其成员;(
->
)- 当然还有第二种方式进行访问,如果
sp
是结构体指针,那么*sp
则表示的是一个结构体,那么此时就可以用.
符进行成员访问了。 ```cpp struct person{ std::string name; int age; };
- 当然还有第二种方式进行访问,如果
person* sp = new person; // a pointer sp -> name = “dhh”; sp -> age = 20;
cout << “name is “ << sp -> name; // method 1 cout << “age is “ << (*sp).age; // method 2
注意:
- 利用`delete`进行空间释放只能释放通过`new`分配的空间;
<a name="9AOY8"></a>
### C++数据内存管理
<a name="kxqV8"></a>
#### 自动存储
在函数内部定义的常规变量使用自动存储空间,被称为自动变量。它们在所属函数被调用时自动产生,在该函数结束时消亡。也就是说自动变量就是一个局部变量,其作用域为包含它的代码块(一对花括号,在函数内部当然也可以产生代码块)。<br />自动变量被存储在栈中,执行代码块时,其中的变量依次加入栈中,出代码块时按相反的顺序进行释放,采用后进先出的方式。
<a name="frVg2"></a>
#### 静态存储
静态存储是整个程序执行期间都存在的存储方式。有两种定义方式:
1. 在函数外面定义变量;
1. 利用关键词`static`;
<a name="TWIU6"></a>
#### 动态存储
动态存储即是由`new`和`delete`提供的。他们管理了一个内存池,称为自由存储空间或者堆。该内存池同用于静态变量和自动变量的内存是分开的。内存的分配和释放都由编程者控制。(所以new的空间,一定要记得释放,否则会发生内存泄漏)
<a name="y2QH7"></a>
#### 线程存储
<a name="NY4o4"></a>
## 数组的替代品
<a name="SUDt8"></a>
### 模板类vector
模板类vector类似于string类,也是一种动态数组,你可以在运行阶段是在vector对象的长度,可在末尾附加新数据,还可以在中间插入新数据(有点像链表咯)。它是使用new创建动态数组的替代品,实际上在其内部实现的确是使用new和delete来管理内存。<br />模板类的特点:
1. 必须包括头文件,比如此处的vector;
1. 必须进行namespace的申明;
1. 模板使用不同的语法来指出它所存储的数据类型;
1. 模板类使用不同的语法来指定元素个数;
vector定义方式:<br />`vector<typeName> vt(n_elem);`<br />注意:没有`n_elem`则默认为0
```cpp
#include<vector>
#include<iostream>
using namespace std;
int main(){
vector<int> vi(1);
vi[0] = 10;
cout << vi[0];
}
模板类array
vecotr类的功能比数组强大,但付出的代价是效率较低,但是与数组相比更安全。array类和数组类似,长度固定,也使用栈(静态内存分配),其效率和数组相同,但是相对于数组来说更安全。
array定义方式:array<typeName, n_elem> arr;
#include<array>
#include<iostream>
using namespace std;
int main(){
array<int, 1> arr;
arr[0] = 10;
cout << arr[0];
}
数组的越界索引
#include<array>
#include<iostream>
using namespace std;
int main(){
int arr1[3] = {1,2,3};
int arr2[3] = {4,5,6};
arr1[-1] = 10; //越界,相对于:*(arr1 - 1)
cout << arr1[0] << arr1[1] << arr1[2];
cout << arr2[0] << arr2[1] << arr2[2];
}
上式代码输出为:1234510;当然也可以用arr2[4]
(相对于*(arr2 + 4))
进行内存元素修改。
利用中括号[]
进行索引vector
和array
同样会出现这样的问题,但是可以采用他们的成员函数进行非法索引捕获。