C++学习ing - 《C++ Primer》阅读和总结

前言

计算机专业研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 = &pi;

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 = &pi;  // 错误:ptr是普通指针
const double *cptr = &pi;
*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 = &pi;  // 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类型
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值