引用

引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。其格式为:类型 &引用变量名 = 已定义过的变量名。

引用特点

  1. 一个变量可取多个别名。
  2. 引用必须初始化。(指针可以任意时候赋值)
  3. 引用只能在初始化的时候引用一次 ,不能更改为转而引用其他变量。(指针可以指向其它变量)
  4. 引用在实现时被编译为const指针;
  5. 不能建立引用数组;

    指针

    指针存储的是变量的地址,而不是像普通变量那样存储的值。再python,java中没有指针类型。C++中还有一个与指针功能非常相似的操作:引用!
    定义指针采用运算符作为标识,在计算过程中仍旧采用进行“解除引用”,即获取指针所指向地址的值。
    1. // 定义指针
    2. int number = 10;
    3. int* pointer = &number; //其中&就是引用运算符,但是此处的意思时取地址,pointer的值等于number的地址
    4. // 利用*进行解除引用操作
    5. cout << "number is " << *pointer << endl;
    6. // 利用指针修改number
    7. *pointer = *pointer + 1
    8. // 定义引用
    9. int& ref = number
    10. // 利用引用修改number
    11. ref = number + 1
    事实上,对指针指向地址的值进行修改即是对原变量进行修改。

    指针定义

    格式:类型 p_name;其中两边的空格可选,通常情况下是*前面不留空格,后面留有空格。指针之间可以互相赋值。

    指针的危险

    定义指针的时候,计算机将分配用来储存地址的空间,但不会分配用来存储指针所指向数据的内存。那么当指针没有被初始化的时候(指针存储的地址为随机值),在后续操作指针将会进行一些了不可控修改。
    所以在定义指针的时候,将指针初始化一个确定的、适当的地址很重要!(可以给指针赋值为0,即null,指向空指针)

    指针和数字

    指针存储的是地址,地址通常被当作整型来处理,但是地址和整数确实两个截然不同的类型。地址不能进行乘除操作等,也不能够地址加地址。
    1. int * pt;
    2. pt = 0xB8000000; // 类型不匹配,会报错
    解决上面的问题,可以进行强制类型转换:
    1. int * pt;
    2. pt = (int *)0xB8000000;
    但是指针可以进行偏移操作,每次偏移1,相当于地址增减和指针类型匹配的一个单位。(例如:int型指针+1操作和double型指针+1操作,一个将会在地址数值上加4,一个会加8)
    由于地址的偏移操作,指针的一种用法和数组非常类似:
    1. int a[10] = {1,2,3};
    2. int * p = a; // a等于a[0]的地址,即数组名和数组首地址对应
    3. // 以下将输出相同的结果
    4. cout << "a[2] is " << a[2] << endl;
    5. cout << "*(p+2) is " << *(p+2) << endl;
    6. cout << "p[2] is " << p[2] << endl;
    可以看到数组和指针具有极大的相似性!

    使用new进行动态分配

    格式:typeName * pointer_name = new typeName;
    这是指针最具有价值的地方之一!为已经被分配内存的变量分配指针只是为该变量赋予一个别名(此时相当于引用),利用new进行未命名空间分配时才能体现指针的价值,此时对该内存的访问只能通过指针实现!(C语言中可以通过malloc()库函数实现,但是比较麻烦)
    值得注意的时,通常变量被储存在栈中(stack),但是new从堆(heap)或自由存储区的内存区域分配内存。
    1. char* cp = new char;
    2. int * int_p = new int;
    3. int* arr_pointer = new int[10];

    使用delete释放内存

    释放内存会释放指针指向的区域,但是不会删除指针本身。delete之后通常需要将指针指向null,防止后续发生问题。并且要注意new和delete配对使用,否则容易发生内存泄漏(内存泄漏
    不要尝试释放已经被释放的内存块,这会导致不确定的结果;并且不能用delete来释放不是通过new分配的内存(变量定义的等)!对空指针进行delete是安全的,所以delete之后可以将指针指向null来预防后续错误!
    1. int * ps = new int;
    2. // 释放内存
    3. delete ps;
    注意事项:
  • delete只能释放通过new分配的空间;
  • 不是通过new分配的空间不能释放;
  • new分配时带[]则,delete时同样应该带[],new分配是没有带[],则delete是也不要带[];

    动态数组的创建

    利用new创建数组又叫做“动态联编”,对于new创建的数组同样可通过delete进行释放。
    其中如果不用”[]”时并不会释放整个数组,只会释放ps指向的元素占用的内存

    1. // 创建数组
    2. int * ps = new int[100]
    3. //内存释放
    4. delete [] ps; // 方括号告诉程序释放整个数组,而不仅仅时指针指向的元素

    一个例子

    1. #include<iostream>
    2. int main(){
    3. using namespace std;
    4. double * p3 = new double[3];
    5. p3[0] = 0.1;
    6. p3[1] = 0.2;
    7. p3[2] = 0.3;
    8. if (*(p3+1) == p3[1]){
    9. cout << "OK!" << endl;
    10. }
    11. p3 = p3 + 1;
    12. if (*p3 == 0.2){
    13. cout << "OK!" << endl;
    14. }
    15. }

    以上代码将输出两个OK!。

    指针、数组和指针算术

    指针和数组基本等价。指针变量加1,则指针的值增加量等于它所指向类型的字节数。数组名被解释为数组的首地址。

    1. int stacks[3] = {1, 2, 3};
    2. int * sp = stacks;
    3. cout << *(stacks + 1) << endl; // stacks[1] == *(stacks + 1)
    4. cout << *(sp + 1) << endl;

    上面的例子说明,”[…]”无论是对数组还是指针来说都是取偏移的操作,数组名完全可以被当做是一个const的指针。
    值得注意的是:数组名相对于数组的首地址,但是对数组名进行&操作将得到的是整个数组的地址(而非首地址),例如:

    1. int tell[10];
    2. cout << tell << endl;
    3. cout << &tell << endl; // 输出结果不同

    其中的tell可以看做是一个int型的指针,但是&tell则被看做是size是int 10倍的数据类型的指针,即:

    1. int (*pas) [10] = &tell // 或者:int (*)[10]类型;

    此时pas和tell等价,(pas)[1]为tell数组第一个元素。

    指针和字符串

    看cout

    1. char flower[10] = "rose";
    2. cout << flower << "s are red?\n";

    其中的flower是数组名,同样也是char数组的首地址,那么cout接受char数组的首地址之后,将会从该字符打印,知道碰到空字符为止;由于指针本身就是保存的地址,所以将char指针传入cout同样会得到同样的效果!与字符数组对应"s are red?\n"同样表示是一个字符数组,它同样表示的是一个地址!
    由于cout的这种性质,如果想要打印地址,则需要进行强制类型转换:(int *)

    1. char flower[10] = "rose";
    2. cout << (int *)flower; // 将会是16进制打印输出

    注:也可以用(int)进行强制类型转换,两者的差别:前者为16进行输出,后者为10进制输出。

    字符串副本

    值得注意的是,数组虽然和指针很类似,但是也存在诸多不同。比如,指针可以指向不同的地方,但是数组名虽然也是保存的一个地址,其地址会在申明时分配,并且不能改变 —> 数组名更像是一个const类型的指针。
    生成数组的副本时,不能采用以下方式:

    1. char mychars[10] = "hello!";
    2. char * cp = mychars; // 只是地址赋值,cp和mychars保存的同一个地址

    正确做法:1、定义指针并新分配空间;2、字符串复制;

    1. char mychars[10] = "hello!";
    2. char * cp = new char[strlen(mychars + 1)];
    3. 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

  1. 注意:
  2. - 利用`delete`进行空间释放只能释放通过`new`分配的空间;
  3. <a name="9AOY8"></a>
  4. ### C++数据内存管理
  5. <a name="kxqV8"></a>
  6. #### 自动存储
  7. 在函数内部定义的常规变量使用自动存储空间,被称为自动变量。它们在所属函数被调用时自动产生,在该函数结束时消亡。也就是说自动变量就是一个局部变量,其作用域为包含它的代码块(一对花括号,在函数内部当然也可以产生代码块)。<br />自动变量被存储在栈中,执行代码块时,其中的变量依次加入栈中,出代码块时按相反的顺序进行释放,采用后进先出的方式。
  8. <a name="frVg2"></a>
  9. #### 静态存储
  10. 静态存储是整个程序执行期间都存在的存储方式。有两种定义方式:
  11. 1. 在函数外面定义变量;
  12. 1. 利用关键词`static`
  13. <a name="TWIU6"></a>
  14. #### 动态存储
  15. 动态存储即是由`new``delete`提供的。他们管理了一个内存池,称为自由存储空间或者堆。该内存池同用于静态变量和自动变量的内存是分开的。内存的分配和释放都由编程者控制。(所以new的空间,一定要记得释放,否则会发生内存泄漏)
  16. <a name="y2QH7"></a>
  17. #### 线程存储
  18. <a name="NY4o4"></a>
  19. ## 数组的替代品
  20. <a name="SUDt8"></a>
  21. ### 模板类vector
  22. 模板类vector类似于string类,也是一种动态数组,你可以在运行阶段是在vector对象的长度,可在末尾附加新数据,还可以在中间插入新数据(有点像链表咯)。它是使用new创建动态数组的替代品,实际上在其内部实现的确是使用newdelete来管理内存。<br />模板类的特点:
  23. 1. 必须包括头文件,比如此处的vector
  24. 1. 必须进行namespace的申明;
  25. 1. 模板使用不同的语法来指出它所存储的数据类型;
  26. 1. 模板类使用不同的语法来指定元素个数;
  27. vector定义方式:<br />`vector<typeName> vt(n_elem);`<br />注意:没有`n_elem`则默认为0
  28. ```cpp
  29. #include<vector>
  30. #include<iostream>
  31. using namespace std;
  32. int main(){
  33. vector<int> vi(1);
  34. vi[0] = 10;
  35. cout << vi[0];
  36. }

模板类array

vecotr类的功能比数组强大,但付出的代价是效率较低,但是与数组相比更安全。array类和数组类似,长度固定,也使用栈(静态内存分配),其效率和数组相同,但是相对于数组来说更安全。
array定义方式:
array<typeName, n_elem> arr;

  1. #include<array>
  2. #include<iostream>
  3. using namespace std;
  4. int main(){
  5. array<int, 1> arr;
  6. arr[0] = 10;
  7. cout << arr[0];
  8. }

数组的越界索引

  1. #include<array>
  2. #include<iostream>
  3. using namespace std;
  4. int main(){
  5. int arr1[3] = {1,2,3};
  6. int arr2[3] = {4,5,6};
  7. arr1[-1] = 10; //越界,相对于:*(arr1 - 1)
  8. cout << arr1[0] << arr1[1] << arr1[2];
  9. cout << arr2[0] << arr2[1] << arr2[2];
  10. }

上式代码输出为:1234510;当然也可以用arr2[4](相对于*(arr2 + 4))进行内存元素修改。
利用中括号[]进行索引vectorarray同样会出现这样的问题,但是可以采用他们的成员函数进行非法索引捕获。