前言
计算机专业研0菜狗,实验室用C++较多,目前按照代码随想录卡哥的C++学习路线学习。看书看过就忘,写博客记录方便记忆。
思维导图(逐步完善中…)
一、变量和基本类型
1.带符号和无符号
类型int、short、long、long long是带符号的,通过在类型名前添加unsigned变为无符号类型。
字符型特殊,分为三种:char、signed char、unsigned char。类型char实际上会表现为有符号或者无符号,具体由编译器决定。
unsigned char c = -1; //假设char占8个比特,c的值为255
signed char c2 = 256; //假设char占8个比特,c2的值是未定义的
- 给无符号类型赋超出范围的值,结果是初始值对无符号类型表示的数值总数取模后的余数。例如:8bit大小的unsigned char可以表示0-255的值,共256个。-1对256取模为255,所以把-1赋值给c实际值为255。
- 给带符号类型赋超过范围的值,结果是未定义的,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
我们不会故意给无符号值赋一个负值,可能会写出这样的代码。
算术表达式中既有无符号数又有int值时,int值会被转换成无符号数(转化过程与直接给无符号变量赋值相同)。在无符号的运算中也应该保证运算结果不会是一个负值。
unsigned u = 10;
int i = -42;
std::count << u + i << std::endl; //如果int占32位,输出4294967264
2.变量
变量提供一个具名的、可供程序操作的存储空间。C++中“变量(variable)”和“对象(object)”一般可以互换使用。
(1)初始值
C++中初始化和赋值是两个完全不同的操作,初始化的含义是创建变量时赋予一个初始值,赋值的含义是把对象的当前值擦除,以一个新值来替代。
给units_sold初始化为0的多种方式:
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);
使用花括号的方式称为列表初始化,重要特点:如果使用列表初始化且初始值存在丢失信息的风险,编译器会报错。
int double ld = 3.1415926536;
int a{ld}, b = {ld}; //错误:转换未执行,因为存在丢失信息的危险
int c{ld}, d = ld; //正确:转换执行,且确实丢失了部分值
(2)默认初始化:定义于任何函数体之外的变量被初始化为0。定义在函数体内部的内置类型变量将不被初始化,试图拷贝或访问此类值会引发错误。绝大多数类都支持无须显示初始化而定义对象,这样的类提供一个合适的默认值。
3.extern
(1)分离式编译
如果将程序分成多个文件,需要在文件间共享代码。C++将声明和定义区分开,声明使得名字为程序所知道,定义负责创建与名字关联的实体。
如果要声明一个变量而非定义,需要使用extern关键字,并且不要显示的初始化变量。
extern int i; //声明而非定义i
int j; //定义并声明j
extern double pi = 3.1416; //定义
在函数体内部如果试图初始化一个由extern关键字标记的变量,将引发错误。
(C++是静态类型语言,在编译阶段检查类型。编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件)
(2)与const结合
默认情况下const仅在文件内有效。使用extern在一个文件中定义const,在多个文件中声明并使用它。
// file_1.cc 定义并初始化一个变量,该变量可以被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize; // 与file_1.cc中定义的bufSize是同一个
4.作用域
- 全局作用域:定义于函数体之外,整个程序范围内都可以使用。
- 块作用域:在函数内可访问。
- 嵌套的作用域:内部作用域&外层作用域。(允许在内层作用域中重新定义外层作用域已有的名字)
#include <iostream>
int reused = 42; //reused拥有全局作用域
int main()
{
int unique = 0; //unique拥有块作用域
//输出1:使用全局变量reused;输出 42 0
std::cout << reused << " " << unique << std::endl;
int reused = 0;
//输出2:使用局部变量reused;输出 0 0
std::cout << reused << " " << unique << std::endl;
//输出3:显式地访问全局变量reused;输出 42 0
std::cout << ::reused << " " << unique << std::endl;
return 0;
}
5.指针&引用
(1)基础
引用:给对象起了另外一个名字,引用和初始值绑定在一起,而不是进行拷贝。
int ival = 1024;
int &refVal = ival; //refVal指向ival
int &refVAL2; //报错:引用必须被初始化
// 定义多个引用
int i1 = 2048, i2 = 1024, &r1 = i1;
int &r2 = i1, &r3 = i2;
指针:存放对象的地址。引用不是对象,没有实际地址,不能定义指向引用的指针。
int dval = 42;
int *pd = &dval; // 取地址操作符&
std::cout <<*pd << std::endl; // 解引用操作符*
(2)空指针
int *p1 = nullptr;
int *p2 = 0;
// 需要首先#include cstdlib
int *p3 = NULL;
NULL是预处理变量给指针赋值,值为0。在C++11标准下最好使用nullptr,避免使用NULL。
(3)void指针
void是特殊的指针类型,可用于存放任意对象的地址。利用void指针的场景有限:拿它和别的指针比较,作为函数的输入或输出,赋值给另外一个void指针。不能直接操作void指针所指的对象,因为不知道对象是什么类型,可以做什么操作。
double obj = 3.14, *pd = &obj;
void *pv = &obj;
pv = pd;
(4)复合
int i = 1024, *p = &i, &r = i;
// 指向指针的指针
int ival = 1024;
int *pi = &ival;
int **ppi = π
std::cout << "The value of ival\n"
<< "direct value: " << ival << "\n"
<< "indirect value: " << *pi << "\n"
<< "doubly indirect value: " << **pi
<< std::endl;
// 指针的引用
int i = 42;
int *p;
int *&r = p; // r是一个对指针p的引用
r = &i; // 让p指向了i
*r = 0; // 修改了i的数值
(5)引用和const
可以对常量引用,但是不能用作修改引用绑定的对象。
const int ci = 1024;
const int &r1 = ci;
r1 = 42; // 错误
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象
int i = 42;
const int &r1 = i; //允许const int&引用普通的int对象
const int &r2 = 42; //正确:常量引用
const int &r3 = r1 * 2; //正确:r3是一个常量引用
int &r4 = r1 * 2; //错误:r4是一个普通的非常量引用
当一个常量引用被绑定到另外一种类型上时发生:
double dval = 3.14;
const int &ri = dval;
// 编译器为了确保ri绑定一个整数,使用了临时量对象,让ri绑定这个临时量
const int temp = dval;
const int &ri = temp;
如果ri不是常量时,执行这样的初始化修改ri的值会修改临时量的值,不会修改dval的值。基本不会想把引用绑定到临时量上,C++语言把这种行为归为非法。
const引用可以引用非常量的对象,但是不能通过const引用进行修改,可以通过其他方式进行修改。
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; // 正确:i的值修改为0
r2 = 0; // 错误:r2是一个常量引用
(6)指针和const
指针的类型必须与其所指向对象的类型一致,但是有两个例外,一个例外是允许另指向常量的指针指向一个非常量对象。
const double pi = 3.14;
double *ptr = π // 错误:ptr是普通指针
const double *cptr = π
*cptr = 42; // 错误:*ptr不能赋值
double dval = 3.14;
cptr = &dval; // 正确:但是不能通过cptr修改dval的值
它们觉得自己指向了常量,所以自觉的不去改变所指对象的值。
const指针
指针是一个对象,所以可以设置常量指针,不能改变的是指针的数值而不是指针指向的数值。
int errNumb = 0;
int *const curErr = &errNumb; // curErr会一直指向errNumb
const double pi = 3.14159;
const double *const pip = π // pip是一个指向常量对象的常量指针
最好的判断方式是从右向左阅读,此例中离errNumb最近的是const所以本身是一个常量对象。声明符的下一个符号是*,说明是一个常量指针。
顶层const
- 顶层const:指针本身是个常量
- 底层const:指针所指的对象是一个常量
int i = 0;
int *const p1 = &i; // 不能改变p1,顶层const
const int ci = 42; // 不能改变ci,顶层const
const int *p2 = &ci; // 可以改变p2,底层const
const int *const p3 = p2; // 靠右的const是顶层const,靠左的是底层const
const int &r = ci; //声明引用的const都是底层const
// 底层const的限制,当进行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换,一般来说非常量可以转换成常量,反之不行。
int *p = p3; // 错误:p3包含底层const定义,p没有
p2 = p3; // 正确:p2和p3都是底层const
p2 = &i; // 正确:int*可以转化成const int*
int &r = ci; // 错误:普通的int&不能绑定到int常量上
const int &r2 = i; // 正确:const int&可以绑定到一个普通的int上
6.constexpr和常量表达式
常量表达式:指值不会改变并且在编译过程就能得到计算结果的表达式。
const int max_files = 20; // max_files是常量表达式
const int limit = max_file + 1; // limit是常量表达式
int staff_size = 27; // staff_size不是常量表达式
const int sz = get_size(); // sz不是常量表达式,因为具体值直到运行时才能获取到
constexpr类型便于编译器来验证变量的值是否是一个常量表达式,一般来说,如果认定变量是一个常量表达式,那就把它声明成constexpr类型。
constexpr int mf = 20; // mf是常量表达式
constexpr int limit = md + 1; // mf + 1是常量表达式
constexpr int sz = size(); //只有当size是一个constexpr函数时才是一条正确的声明语句
- 一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象
- 函数体内定义的变量一般不会存放在固定地址中,定义于所有函数体之外的对象地址固定不变,constexpr的引用和指针可以指向这种变量。
constexpr把它所定义的对象置为了顶层const
const int *p = nullptr; // p是一个指向整形常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
constexpr int *np = nullptr;
int j = 0;
constexpr int i = 42;
// i,j定义在函数体之外
constexpr cosnt int *p = &i; // p是常量指针,指向整形常量i
constexpr int *p1 = &j; // p1是常量指针,指向整数j
7.处理类型
(1)类型别名的定义方式
typedef double wages; // wages是double的同义词
typedef wages base, *p;
// 别名声明
using SI = Sales_item;
// 使用类型pstring为char*的别名
typedef char *pstring;
const pstring cstr = 0; // cstr是指向char的常量指针
const pstring *ps; // ps是一个指针,它的对象是一个指向char的常量指针
// pstring是指向char的指针,const pstring是指向char的常量指针,而非指向常量字符的指针。
(2)auto类型
// auto定义的变量必须有初始值
auto item = val1 + val2; // item初始化为val1和val2相加的结果
// 一条声明语句只能有一种类型
auto i = 0, *p = &i; // 正确
auto sz = 0, pi = 3.14; // 错误,sz和pi不同类型
auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
int i = 0;
const int ci = i, &cr = ci;
auto b = ci; // b是整数(ci的顶层const特性被忽略掉)
auto c = cr; // c是整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; // d是一个整形指针
auto e = &ci // e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
// 需要判断出auto类型是一个顶层const,需要明确指出
const auto f = ci; // f是const int
// 引用的初始化规则同样适用
auto &g = ci; // g是一个整形常量引用,绑定到ci
auto &h = 42; // 错误:不能为非常量引用绑定字面值
const auto &j = 42; // 正确:可以为常量引用绑定字面值
(3)decltype类型指示符
decltype的作用是选择并返回操作数的数据类型。
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
编译器不会实际调用函数f,而是使用调用发生时f的返回值类型作为sum的类型。decltype处理顶层const和引用的方式与auto有差别,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&,y绑定到变量x
decltype(cj) z; // 错误:z是一个引用,必须初始化
有些表达式将向decltype返回引用类型:
// decltype的结果可以是引用类型
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // 正确:加法的结果是int,因此b是一个未初始化的int
decltype(*p) c; // 错误:解引用后c是int&,是引用类型必须初始化
decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型,如果加上括号会当成一个表达式,会得到引用类型。
decltype((i)) d; // 错误:d是int&,必须初始化
decltype(i) e; // 正确:e是int类型