引用
引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。其格式为:类型 &引用变量名 = 已定义过的变量名。
引用特点
- 一个变量可取多个别名。
- 引用必须初始化。(指针可以任意时候赋值)
- 引用只能在初始化的时候引用一次 ,不能更改为转而引用其他变量。(指针可以指向其它变量)
- 引用在实现时被编译为const指针;
- 不能建立引用数组;
指针
指针存储的是变量的地址,而不是像普通变量那样存储的值。再python,java中没有指针类型。C++中还有一个与指针功能非常相似的操作:引用!
定义指针采用运算符作为标识,在计算过程中仍旧采用进行“解除引用”,即获取指针所指向地址的值。
事实上,对指针指向地址的值进行修改即是对原变量进行修改。// 定义指针int number = 10;int* pointer = &number; //其中&就是引用运算符,但是此处的意思时取地址,pointer的值等于number的地址// 利用*进行解除引用操作cout << "number is " << *pointer << endl;// 利用指针修改number*pointer = *pointer + 1// 定义引用int& ref = number// 利用引用修改numberref = 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同样会出现这样的问题,但是可以采用他们的成员函数进行非法索引捕获。
