跳转至

C++

作者插入

这个分类的内容主要是用来记录C++语法的,当然也不可能详细地介绍,只是有感而发。当然,如果有什么好的想法要我写的话,请转到Github的Issues提交问题。

指针和数组

指针是什么

指针是一种C/C++变量,其存储的信息是变量的地址变量的类型信息。每种类型,包括自定义类型,系统都会自动派生出对应的指针类型(在变量前加*),其定义只要在参数前加上*就可以了。

在这里,我们需要注意以下几点:

  • 变量地址指该变量所占内存空间的首地址。例如int a占了内存105-108,则a的地址为105.

  • 当该变量为自定义类型时,其所占的空间为其成员所占的空间和,不包含动态申请的空间,一个特定的类具有唯一确定的空间大小(不包括动态申请的内存空间)。例如有以下类型。则其所占的空间为12Bytes(假设一个指针占4Bytes),获取一个变量或者类型占用的内存空间可以用sizeof()

1
2
3
4
5
6
class A
{
    string* a;
    int* b;
    long long* c;
};
- 类和对应的指针的&*运算符默认为取地址和解指针运算符,当然也可以根据实际需要来重载。

指针的+-运算符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include<iostream>
using namespace std;
int main()
{
    int a[2] = {1,3};
    int *p = a; //定义一个指针变量p,其指向数组的第0个元素,就假设其地址值为101(当然,实际上是16进制的).
    cout << p << endl; //输出101.
    p = p + 1; //p后移以为,指向数组的第1个元素.
    cout << p << endl; //输出105(假设int占4Bytes).
    cout << *p << endl; //输出3.
    return 0;
}

读者须知

在涉及到讲解的代码块时,推荐自己先跑一遍代码,然后与本文所讲的进行对比。

int*(a) + int(b)表示指针的地址向后移动b * sizeof(int)位,也就是b个int型的长度,这样做是为了方便数组和移位的操作。

当然int*(a) - int*(b)和上面类似,表示中间相差的元素个数。

指针的下标运算

指针的下标运算和数组的很像,实际上下标运算通常在动态数组中应用,例如a[2]表示动态数组a的第二个元素。根据上文的运算法则,a[2]<==>*(a+2),其两者实现的效果等价。

奇怪的"类型"-数组

作者插入

其实本来我想先讲4再讲3,但既然提到了数组,那就讲讲数组这个奇怪的东西。

数组并不是一个真正的类型。而是一个不完全类型,静态数组的容量大小只能在编译前确定,而且它还具有很多奇怪的性质。

数组是用一块连续的内存空间来存储的,因此,C++的数组可以通过数组名来获取其首地址,用下标运算获取对应的元素。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include<iostream>
using namespace std;
int main()
{
    int a[5] = {1,2,3,4,5};
    cout << sizeof(a) << endl;
    sizetest();

    return 0;
}

void sizetest(int a[]) //请看这个,入参其实和int *a没什么两样。
{
    cout << sizeof(a) << endl;
}

你会发现,打出来的两行东西是不一样的,第一行是20,第二行是4。这是因为前一个sizeof()可以通过上下文定位数组(也就获取到了它元素的个数),而函数中的sizeof()无法获取数组的长度信息(因为已经是一个指针了),也就是说,数组只能通过指针的方式传递,且会丢失信息。

二级指针和指向数组的指针

提示

二级指针包括指针的指针,也包括指向数组的指针。当然,这里的二级指针的含义指指向指针的指针。

1
2
3
4
5
6
7
8
9
#include<iostream>
using namespace std;
int main()
{
    int a[4][10];

    int **pa = a; //int(*)[10]类型的值不能初始化int**的实体
    int (*pa)[10] = a; //通过
}

第一个就是二级指针,第二个就是指向数组的指针,这个很少用,真的很少用,当然,请注意int (*pa)[10]int *pa[10]的区别,一个是指向数组的指针,一个是基类型为int*的一个数组。

引用

引用的概念

C++指针使得我们可以直接和内存进行交互。然而,有时候我们仅仅需要一个传递一个变量,而不关注其他的内容。所以C++设计了引用,从而减少了&*的操作。

引用是一种已经包装过的常指针,其很多特性和常指针很像,然而其不需要复杂的&*操作。引用可以形象化地理解为给变量取别名。当引用传递给参数时,可以理解为把整个变量给了函数。

因为引用本质上是一个常指针,因而 1)不能用一个常量来给引用赋值。 2)引用定义时就要初始化,且引用的变量在中途不能更改。

引用传递和值传递

无论是普通基类对象还是指针,当函数调用或者返回的时候都属于值传递的方式,当参数是类对象时,其会调用拷贝构造函数来生成一个副本,作为函数内的临时对象。而指针是通过拷贝地址的方式来间接实现传递变量的。

而引用虽然本质上是常指针,但是从形式上来说其确实传递了变量,因而当一个参数为引用时,其参数必须是可修改的左值。例如参考以下代码,其输出将为10。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void add(int& a)
{
    a = a + 10;
}

int main()
{
    int a = 0;
    add(a);
    cout << a << endl;
    return 0;
}

引用的作用

引用就其功能来说完全可以通过指针来实现,但是引用的传递参数更加地方便,也提高了程序的阅读能力。其中cin >>便是一个很好地使用了引用的例子,因为流输入是个变量赋值的。

其调用参数和使用参数的时候和普通的对象几乎没有什么区别,所以也提高了代码的可维护性,引用在必要的地方还可以节省一次创建拷贝的过程,节省了内存空间。

const修饰符

const表明了的属性,所以要理解const,就需要理解这个概念。const可以修饰变量,指针,函数和成员/字段,我们将非const赋给一个const对象,但是反过来就不可以。

修饰变量

这里的变量不包括指针,既可以是基类型,也可以是类对象。当然,当一个变量用const来修饰时,表明其成员无法修改,对于类对象来说,其还有不能调用非const函数的特征(因为const函数可以保证其数据成员不修改)。

除此之外,通过#define也可以实现类似的功能,但是其原理是通过文本替换来实现的。

修饰函数

当一个函数声明为const时,其表示这个一个不会修改数据成员的函数,任何企图修改数据成员的函数都会编译不通过。这样可以确保const对象调用的方法不会修改成员。

提示

虽然const函数无法修改数据成员,但是如果其成员中有一个指针,则其指针指向不能变,但仍然可以通过指针来修改内存中的数据。

修饰指针

指针有两处地方可以修饰,一种是指针符号前,另一种是指针符号后,其区别如下

const int *p表示指向const数据的指针。其指针的指向可以修改,但是不能通过该指针修改其内存中的值(并没有要求其指向的是一个常量)

int * const p是一个常指针,其指向不能更改,但是可以通过该指针修改内存中的值

提示

const int *pint const *p属于同一种东西,是指向常量的指针变量。

const设计出来主要用来 1)定义一个全局的变量 2)防止传递参数时函数内部修改变量的值。

浅拷贝和深拷贝

对于一般的类型来说,编译器默认的拷贝就是深拷贝。然而当类型包含指针类型的数据时,系统默认会拷贝指针的值,而不会去拷贝其内存中的值。这种通过拷贝后数据仍然是一份(占用同一块)内存空间的现象就叫做浅拷贝。浅拷贝可以减少变量所占的空间(因为需要拷贝的数据少了),但是会导致很多问题。

而问题主要出析构函数这一块,析构函数希望通过撤销动态空间来防止内存泄露,但是这样做会出现野指针的现象。

后续,我会通过例子来讲解浅拷贝深拷贝的实际作用。

C++迭代器1


  1. 此篇文章参考该文章