最近开始看 《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 指针,不可改变指向的对象。
指针
指针是一个变量,它本身需要分配内存空间,同时存储的是一个地址。它的内存大小由机器的寻址长度决定。比如机器的寻址长度为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 ,因为头文件被其他文件包含之后会产生歧义。