初始化和赋值
在c++中初始化和赋值是两个不同的概念
初始化是在创建变量时赋予一个初始值
而赋值则是赋予一个值去擦除原先的值
但在其他语言中,初始化和赋值的区别并不大
引用
引用实质上是另外一个变量的别名
因此它们共用的是同一块内存区
但由于引用仅仅是个名字,并不是对象,因此不能创建引用的引用
指针
指针存放的是某个变量的地址
取地址符& 解引用符*
指针的值应属于四种状态之一:
1.指向一个对象
2.指向紧邻对象所占空间的下一个位置
3.空指针,即没有指向任何对象
4.无效指针,除了上述三种情况外
空指针
空指针不指向任何对象,通常在使用指针的时候我们都会对他先进行检查看是否为空
生成空指针有如下几个方法
-
直接int *p1 = 0;
-
采用字面值nullptr
int *p1 = nullptr;
nullptr 是c++11标准引入的
注意!
我们赋值空指针直接赋予0并不等同于我们可以将一个值为0,int类型的变量赋值给一个指针
指针的值始终是一个地址!绝对不能直接赋值变量
指针初始化
指针不初始化的话,会随机指定到系统的一块内存空间,这样很容易带来错误!
因此当你要定义指针的时候需要对其进行初始化
即使还没有对应的变量地址能用于初始化,也要把指针初始化为一个空指针
指针的其他操作
指针用于条件表达式当中
若指针的值是0,则对应False,其他情况为True
指针的比较操作
当两个指针存放的地址相同时,则比较时两者是相等的。
void *指针
它可以存放任意对象的地址
但其操作有限,只能:
1.和别的指针进行比较
2.作为函数输入输出
3.赋值给另外一个Void *指针
复合类型
定义多个变量
我们最好是采用以下的写法
int *p1, p2;
我们定义了一个指针p1和一个int类型变量p2
int* p1, p2;
这段代码和上面等效,但容易让人产生误解,会让我们以为p2是int类型指针
指针的指针
指针指向一个对象的地址
同样我们可以定义一个指针p1,指向另外一个指针p2,即存放p2指针的地址
注意这里我们定义指针的指针需要用到双星号**
int i = 21;
int *p1 = &i;
int **p2 = &p1;
cout<<p1<<", "<<*p2<<endl;
cout<<i<<", "<<**p2<<endl;
输出结果:
0x61ff08, 0x61ff08
21, 21
指向指针的引用
指针是一个对象,因此我们可以定义指针的引用
int i = 224;
int *p = &i; // 定义一个指向i的指针
int *&r = p; // 定义一个p指针的引用
*r = 256;
cout<<"i的值为: "<<i<<" p的值为: "<<*p<<" r引用的值为: "<<*r<<endl;
输出结果为:
i的值为: 256 p的值为: 256 r引用的值为: 256
Const限定符
const定义的变量值不能被改变,也就是我们常说的常量限定符
当用一个对象去初始化另外一个对象的时候,则它们是不是const都无关紧要
int i = 42;
int j = 199;
const int c1 = i; // 初始化,因此不会报错
c1 = j; // 此时才会报错,因为我们尝试去修改const变量
Const对象仅在独立文件内部有效
C++规定const对象仅在文件内部有效
当程序包含多个文件,且是同名的const变量时,其实等同于不同文件中定义独立的const变量
如果我们定义的const对象想让其在其他文件共用
需要在定义前面加一个关键字extern
const的引用
即把引用绑定到const对象上
下面来看几个初始化
const int c1 = 1024;
const int &r1 = c1; // 将常量转换为一个常量引用
int c2 = 1203;
const int &r2 = c2; // 将一个int类型转换为一个常量引用
int &r3 = c1*2; // 错误,非常量引用的初始值必须为左值
左值和右值
简单判别左值和右值的方法就是,能对表达式取地址的就是左值,不能的话就是右值
OK回到那个初始化错误的地方,因为r3是一个非常量引用,它初始化需要的是一个左值,而c1则是一个常量,是个右值,因此无法初始化。
我们提到过引用的类型必须与其引用对象的类型保持一致,当然也有例外发生
注意这里是在常量引用里面允许的例外情况
double dval = 3.13;
const int &r1 = dval;
如果不是常量引用的话这句初始化其实是错误的,因为我们不能用double类型去初始化一个int类型。
但是在常量这里c++有一个隐式转换
double dval = 3.13;
const int temp = dval;
const int &r2 = temp;
它会先转换成一个const int类型,再转换成一个const int引用
这种转换常常会发生,比如在函数调用里面,如果我们的输入不规范但恰好符合这种自动转换的话,可能你输入进去的就变成一个常量引用,从而引起错误。避免这种错误的最好办法就是在输入的时候保持相同的变量类型
const可能引用的是一个非const对象
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0;
cout<<"i: "<<i<<" r2: "<<r2<<endl;
r2 = 21; // 报错!
输出结果:
i: 0 r2: 0
我们可以通过其他途径修改i的值
但不能通过r2来修改
指向常量的指针
这个与常量的引用类似
它并不要求指向常量的指针其指的对象必须是常量,你可以是别的变量,只不过是不能通过该指针去修改
const double cst_pi = 3.14;
double pi = 3.14;
const double *p1 = π
const double *cst_p2 = &cst_pi;
*p1 = 1324; 报错!
const指针
常量指针必须初始化
其值就一直指向该地址
注意写法不要与指向常量的指针混淆
int err = 256;
const int *p1 = &err; // 这是指向常量的指针
int *const p2 = &err; // 这是const指针,p2一直就指向err的地址
const double *const pip = π // pip是一个指向常量对象的常量指针
把*放在const关键字之前说明指针是一个常量
指针本身是一个常量并不代表不能通过指针去修改其指向对象的值,关键要看对象的类型
*p2 = 12222; // p2所指的是一个int类型变量,因此可以通过p2来修改
*pip = 1232; // pip所指的是一个常量,因此会报错
顶层const和底层const
顶层const表示指针本身是一个常量,而底层const表示指针指向的对象是一个常量
更一般的,顶层const可以表示任意的对象是常量
const int *const p2 //靠左的是底层const,靠右的是顶层const
执行对象拷贝的时候,顶层const几乎没什么影响
而如果是底层const的话,两者需要有同样的底层const资格,或者两个对象的类型可以转换,比如常量可以转换成变量
int p1 = 128;
const int *p2 = &p1; // 正确,const int引用可以绑定到int类型上
int *const p3 = &p1; // 正确,这是一个顶层const
int j1 = 256;
const int *p4 = &j1;
p2 = p4; // 正确,两者都是底层const,我们将p4所存放的地址赋值给指针p3
字面值类型
字面值类型一般比较简单,值显而易见,容易得到,因此称其为字面值
字面值主要包括算术类型,引用,指针
constexpr变量
在一个复杂系统中我们很难去确认一个初始值到底是不是一个常量表达式
在C++11标准中,我们使用constexpr变量来验证变量的值是否是一个常量表达式
constexpr int mf =20;
一般来说,如果认定变量是一个常量表达式,那就用constexpr去声明它
另外如果是constexpr声明的指针,它仅对指针有效,而对指针所指对象无效
比如constexpr int *q 代表的就是q是一个int类型的常量指针
注意的是constexpr指针的初始值必须是nullptr或者0,或者存储与某个固定地址的对象
处理类型
别名
我们可以另起一个别名来达到同义的目的
传统方法是使用typedef限定符号
typedef double DOUBLE; //将DOUBLE定义为double的别名
DOUBLE i = 2300;
typedef double *dd; // 将dd定义double *的别名
dd p = &i;
cout<<*p<<endl;
另外可以使用别名声明
就是使用using xx = 已有名;
using Double = double;
Double a = 1024;
当不是常量的时候,我们理解别名是可以将对应的同名替换到原语句进行理解
但是如果出现const的话,这种理解方法就是错的
typedef char *p;
const p cstr = 0;
如果按照往常思路进行替换,则语句变为
const char * cstr = 0;
你可能会以为这是一个指向char的常量指针
而实际上const和char融合在一起,是const char
因此是一个指向const char的指针
注意!在有常量的地方慎用别名
auto类型说明符
auto类型能让编译器帮我们分析表达式的类型
auto可以用来声明多个变量,因为一条声明语句只能一个基本数据类型,所以声明的变量类型都是相同的
auto i = 1231, j = 3.14; 报错!因为i是int类型,j是double类型,用auto声明会冲突
复合类型
当用auto来推断引用的类型的话,它实质是根据引用对象的类型来作为auto的类型
另外auto会忽略顶层const而保留底层const特性
const int i = 123;
auto r = i; // r 是一个整数, 忽略了顶层const
r = 124;// 因此我们可以对r进行修改
// 如果要保留顶层const需要明确指出
const auto p = i;
// p = 123; // 报错,因为我们保留了顶层const特性,所以p的值无法被修改
decltype类型指示符
decltype是选择并返回操作数的类型(包括顶层const和引用在内)
它可以得到类型,但不进行计算
int i = 8;
decltype(i) a = 129;
cout<<typeid(a).name()<<endl;
如果表达式内容是解引用,则decltype得到的是解引用类型
int i = 12;
int b = 23;
int *r2 = &i;
decltype(*r2) a = b;