C++

变量与基本类型

"实践出真知"

Posted by JosonChan on 2020-11-01

最近开始看 《C++ Primer》,希望对C++重新回顾一下。需要强调一点,数据类型告诉我们数据的意义以及能够在数据上进行的操作。

[toc]

基本类型的选择

  • 如果知道值非负,选用无符号类型
  • 尽量使用 int 而非 short,如果范围超过 int ,则使用 long long
  • 算术表达式中尽量不使用 char 和 bool
  • 浮点数尽可能使用 double 而非 float

声明与定义

声明(declaration)规定了变量的类型和名字,简单来说就是告诉编译器存在这个东西。而定义(definition)的除了声明之外,还要给变量分配内存,也有可能为变量赋一个初始值,也就是为这个变量创建一个实体。

如果想要声明一个变量而非定义它,需要在变量前加上 extern。

extern int i;
int j;
extern int i = 0;// 初始化之后声明失效,也就变成定义了。

首先我们要知道不能够重复定义一个相同名字的变量:

int i = 0;
int i = 1;

编译之后:

main.cpp:197:5: error: redefinition of ‘int i’
 int i = 0;
     ^
main.cpp:195:5: note: ‘int i’ previously defined here
 int i = 1;

如果想要在多个文件中使用同一个变量,就要把声明和定义分离。这个共享变量只能有一次定义,在其他地方使用的时候就要声明它。

test.hpp写上

#pragma once
#include <iostream>

int i = 100;

main.cpp

#include <iostream>
#include "test.hpp"
int main(){
    extern int i;
    std::cout << i << std::endl;
}

指针与引用

引用

引用指的是一个变量的别名,通过引用可以把一个别名绑定到一个变量上。一旦初始化完成,引用将与引用的变量一直绑定在一起,不可改变。因为无法重新绑定,所以引用必须初始化。此时对 b 的修改都将直接影响 a。

int a = 10;
int &b = a;

double c = 3.3;
int &d = c;// 错误,类型不匹配

除了const 引用之外(还有一种情况)之外,引用的类型都要和与之绑定的对象严格匹配。引用经常用在作为函数的参数进行传递,当我们希望传入的参数在函数内直接发生改变的时候,使用引用可以做到。同时这样做的好处在于,按值传递参数的时候,需要创建临时变量,然后将值赋给临时变量,如果说是我们自己定义的类的话,就需要调用拷贝构造函数,这种时候会非常耗时而且浪费,于是可以使用引用来解决。

#include <iostream>
using namespace std;
void fun(int &a){
    a++;
}
int main(){
    int a = 10;
	fun(a);
	cout << a << endl; 
}

同时如果我们不希望传递进去的参数发生改变,可以使用 const进行限制,这样即减少了时间浪费,也避免了参数被修改。

void fun(const int &a);

引用的底层实现

原本我以为引用是变量的别名,在内存中操作的是同一个地址。但是通过实验发现并不是:

#include <iostream>
int main(){
   int a = 10;
   int &b = a;
   int *c = &a;

   b++;
   (*c)++;
}

汇编代码:

push    rbp
mov     rbp, rsp
mov     DWORD PTR [rbp-20], 10
lea     rax, [rbp-20]
mov     QWORD PTR [rbp-8], rax
lea     rax, [rbp-20]
mov     QWORD PTR [rbp-16], rax
mov     rax, QWORD PTR [rbp-8]
mov     eax, DWORD PTR [rax]
lea     edx, [rax+1]
mov     rax, QWORD PTR [rbp-8]
mov     DWORD PTR [rax], edx
mov     rax, QWORD PTR [rbp-16]
mov     eax, DWORD PTR [rax]
lea     edx, [rax+1]
mov     rax, QWORD PTR [rbp-16]
mov     DWORD PTR [rax], edx
mov     eax, 0
pop     rbp
ret

我们可以看到引用和指针都开辟了一个内存空间,分别是[rbp-8]以及[rbp-16],后面的自增操作也是在这两个地址上进行的。所以从汇编的角度看,其实都是需要开辟内存的。但是从语言的角度上看,可以把引用理解成一个const 指针,不可改变指向的对象。

详解:简谈 C++ 中指针与引用的底层实现

指针

指针是一个变量,它本身需要分配内存空间,同时存储的是一个地址。它的内存大小由机器的寻址长度决定。比如机器的寻址长度为64位,则指针的内存大小为8个字节。同时指针具有不同的类型,比如 int ,double,因为不同的类型的变量占用的内存大小不一样,因此在指针操作的时候(比如自增运算符)需要知道往前移动多少。

int a[10] = {0};
int *b = a;
double c = 1.0;
double *d = &c;
cout << &a << endl;// a数组的地址
cout << b << endl;// b的值,也就是a数组的地址
cout << &b << endl; // b本身的地址
cout << (++b) << endl;// 输出自增之后的地址,也就是 a[1],一个 int 占四个字节
cout << &b << endl;// 地址是不会变的,因为存储的b这个变量的地址
cout << sizeof(b) <<endl;
cout << sizeof(d) <<endl;//  两个不同类型的指针变量占用内存空间相等

得到输出:

0x7ffffa8919d0
0x7ffffa8919d0
0x7ffffa8919b8
0x7ffffa8919d4
0x7ffffa8919b8
8
8

同时,指针还可以变量生命期间指向不同的对象,也无须赋初始值。

int a = 10;
int *c = nullptr;
int *d = &a;
*d = c;

void* 指针

void* 指针可以存放任意对象的地址。但是我们不能够知道它的类型是什么,也就是说我们会给这个 void* 指针分配一个寻址长度大小的内存空间,然后存放一个地址,但是我们并不知道它的类型,也就不清楚它能够支持的操作。

利用void* 指针只能用来作为函数的输入和输出,或者赋给另一个void*指针,拿它和别的指针相比较,不能操作它的对象。

double obj = 3.14,*pd = &obj;
void* test = pd;
test = &obj;

复合类型的声明

基本类型的修饰符有 &*,需要注意的是每一个修饰符只对一个变量起作用:

int *a = nullptr,b = 10;// 这里a 是一个int型的指针, b是int型

引用本身不是一个对象,因此不能定义指向引用的指针。但是指针是对象,所以存在对指针的引用。面对一条比较复杂的指针或者引用的声明语句时,从右到左进行阅读。

int *a = nullptr;
int &*b = a;// 从右开始,b是一个指针,但是不存在指向引用(int &)的指针,因此错误
int *&c = a;// 从右开始,c是一个引用,存在一个指针变量的引用

const 关键字

const 修饰变量之后,该变量不能被改变,同时 const 被创建之后其值不能被改变,因此const 变量一定需要初始化。

const int a = 10;// 正确
const int b; // 错误,没有初始化
int c = a;// 可以使用一个const int 取初始化 int,只要不存在有改变 const int 的值的操作,顶层const

当以编译时初始化的方式定义一个 const 对象时,编译器将在编译中把用到该变量的地方都替换成对应的值。也就是编译器会找到代码中所有使用该const变量的地方,用初始化数值进行替换:

#include <iostream>
int main(){
    const int a = 100;
    int b = 100;
    int c = a;
    int d = b;
}

push    rbp
mov     rbp, rsp
mov     DWORD PTR [rbp-4], 100 # 创建 const a 变量,初始化 100
mov     DWORD PTR [rbp-8], 100 # 创建 b 变量,初始化 100
mov     DWORD PTR [rbp-12], 100 # 编译器直接将100 替换成 a
mov     eax, DWORD PTR [rbp-8] # 由于不是 const,所以先使用了ax寄存器
mov     DWORD PTR [rbp-16], eax
mov     eax, 0
pop     rbp
ret

const 仅在本文件内生效

涉及到编译和链接部分,等学习 CSAPP 再来详解。

const 引用

const 引用,也就是对 const 常量的引用(reference to const):

const int a = 10;
const int &b = a;// 正确 
int &c = a;// 错误,一个常量的引用不可指向一个非常量引用

在初始化const 引用时允许用任意表达式作为初始值,只要该表达式的结果能够转换成引用的类型即可。允许令一个常量的引用绑定一个非常量的对象。

#include <iostream>
int main(){
    double b = 10.0;
    const int &a = b; # # 这里是 double, 因此会创建一个临时量 const int temp = b;再把 a 绑定到 temp 上。
    b = b + 10;
    std::cout << a << std::endl;# 输出还是为10
}
# 汇编结果
push    rbp
mov     rbp, rsp
sub     rsp, 32
movsd   xmm0, QWORD PTR .LC0[rip]
movsd   QWORD PTR [rbp-8], xmm0 # b变量开辟了[rbp-8]
movsd   xmm0, QWORD PTR [rbp-8]
cvttsd2si       eax, xmm0
mov     DWORD PTR [rbp-20], eax # 由于创建了一个临时量[rbp-20]
lea     rax, [rbp-20]
mov     QWORD PTR [rbp-16], rax # 引用开辟了一个内存空间,绑定了临时量 [rbp-20]
mov     rax, QWORD PTR [rbp-16]
mov     eax, DWORD PTR [rax]
mov     esi, eax
mov     edi, OFFSET FLAT:_ZSt4cout
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov     esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov     rdi, rax
call    std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
mov     eax, 0
leave
ret

通过以上的分析可以知道,如果初始化引用的时候使用了不同的类型会导致引用绑定到临时量,对临时量的修改并不能改变原来的那个值,也就和我们的初衷相反了。而由于const引用是一个常量,没有影响,所以允许。但是常量引用仅对引用可以参与的操作做出了决定,对于引用的对象本身是不是常量没有做限定,因为对象可能并非是一个常量,允许通过其他途径修改。

#include <iostream>
int main(){
    int b = 10;
    const int &a = b; // int 类型转常量引用并没有绑定临时量。
    b = b + 10;
    std::cout << a << std::endl;// 输出为 20 。
}
### 指向常量的指针

指向常量的指针(pointer to const),指的是这个变量是一个指针,但是指向一个常量, 所以不能修改指针指向对象的值,但是可以修改指针指向谁。

const int a = 10;
const int *p = nullptr;
p = &a;
(*p)++;// 错误不能修改值

和对常量的引用一样,允许令一个指向常量的指针指向一个非常量的对象。

和对常量的引用一样,指向常量的指针仅仅是该变量不能修改指向对象的值,而没有规定那个对象的值不能通过其他方式更改。

int a = 10;
const int *p = &a;// 指向一个非常量的对象
a++;
cout << *p << endl;// 输出为 11

常量指针

常量指针(const pointer),由于指针本身是一个对象,因此就存在指针的常量,意思就是该指针的值不能改变,也就是指向地址不能改,也就是指向的对象不能改变,所以常量指针必须初始化。

int a = 10;
int *const b = &a;
(*b)++;//可以修改
b = nullptr;// 不允许修改

指向常量的常量指针

也就是说,值不能修改,指向对象不能修改。

const int *const p = nullptr;

顶层 const 与 底层 const

  • 顶层const指的是该变量本身是常量,不可改变。一般的数据类型或者常量指针都是顶层的const。

  • 底层const指的是指向的对象不可改变。指向常量的指针以及绑定常量的引用都是底层的const。

在执行拷贝操作的时候,顶层const可以忽略,但是底层const不可忽略,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型能够转换。一般来说,非常量可以转为常量,反之则不行。

const 的实现

由编译器检查。在PC上,const变量在RAM里,但是像

const char *a="hello world";

这样声明的变量,系统会加载到内存并把页面属性设成只读,const_cast 后改写程序会崩溃(如果实在想改可以用系统API把页面属性改成读写)。如果是const传递的C++对象,const_cast后改写没什么问题。
在某些设备上,const 修饰的变量可能会存在ROM/Flash里,这些存储介质不能通过内存写操作这届改写,因此万一存在只读介质上,const_cast以后强制写会发生未定义的行为,一般是崩溃掉。

引用:const实现原理

constexpr 变量

C++11 规定,允许将变量声明为constexpr类型以便编译器来验证变量的值是否是一个常量表达式,一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr。

类型别名

typedef 关键字以及C新增的 using 关键字,在C11中尽量使用 using。

typedef double wages;
using wages = double;

在C++11中尽量使用 using,同时需要注意复合类型的别名,比如指针或者引用的别名。

using intptr = int*;
int b = 10;
const intptr a = &b; // 不能简单替换!!!需要注意intptr 就是指针类型,不可将它变成 const int *a !!!
(*a)++;// 它是常量指针!!!

auto 与 decltype

C++ 11新增的一个类型,可以根据编译器自行推算出结果的类型。同时,对于顶层const一般会忽略掉,同时底层的const会保留。同时需要初始化它。

当你需要某个表达式的返回值类型而又不想实际执行它时用 decltype。

int a = 8,b = 3;
auto c = a + b;
decltype(a+b) d;

头文件保护符

头文件没有被包含的话,也需要设置保护符,需要养成习惯,防止变量函数类重复定义。

有两种,一种是从C语言继承来的预处理变量,另一种是 #pragma once

#ifndef TEST_HPP
#define TEST_HPP
// 定义与声明
#endif

#pragma once

在编写头文件的时候,尽量不要使用 using namespace ,因为头文件被其他文件包含之后会产生歧义。