本文介绍了C++的基础知识点。包括但不限于:
- 顶层const与底层const
- constexpr
- auto
- decltype
- struct
- 头文件保护符
第一章 开始
include指令(P6,1.2)
通常情况下#include
指令必须在所有函数之外。include和它想包含的头文件名字必须在同一行里,不然会报错。
一般情况下我们把include指令放在源文件代码内容的最前面,当你在源文件中使用#include
声明了一个头文件,效果相当于你把整个头文件黏贴到对应的那一行上。
编译器(P14,1.4)
编译器的一部分工作是寻找程序文本中的错误。
常见错误类型:
- 语法错误(syntax error)
- 类型错误(type error)
- 声明错误(declaration error)
“编辑-编译-调试”(edit-compile-debug)周期。
文件重定向(P19,1.5)
|
|
术语表(P23)
- 花括号 curly brace
- 内置类型 built-in type
- 形参列表 parameter list
- 字符串字面值常量 string literal
- 操作符 manipulator
- 变量 variable
- 初始化 initialize
- 注释 comments
- 集成开发环境 Integrated Developed Environment,IDE
- 条件 condition
- 赋值 assignment
- 表达式 expression
- 语法错误 syntax error
- 方法(类方法) method
第二章 变量和基本类型
C++语言关于类型的规定(P30,2.1.1)
C++语言的基本类型的设定与硬件紧密相关,因此很多类型的内存尺寸也都只是给了一个范围,其实各家IDE(LLVM,GCC,Visaul C++)的实现都是在范围内,具体的实现细节都是不确定的。
其中bool最小尺寸未定义,char最小尺寸是8位,wchar_t和char16_t的最小尺寸都是16位,char32_t的最小尺寸是32位,int的最小尺寸是16位,long和long long的最小尺寸分别是32位和64位,对于浮点型数据的表现尺寸是按照精度计算的,其中float的最小尺寸精度是小数点后6位(通常占内存32bytes),double(通常占内存 64bytes)和long double(通常占内存 96~128bytes)的最小尺寸精度则是小数点后10位(实际可能比这个精度要大一些,比如float小数点后有效位为7,double为16)。
int不得小于short,long不得小于int,long long不得小于long。float,double,long double也应该是精度递增(或者相同)的关系。
要特别注意的是,扩展的字符类型(char16_t,char32_t,wchar_t)和布尔类型都没有带符号和无符号之分(尽管它们也确实属于算数类型)。
类型转换(P32,2.1.2)
程序自动执行的类型转换操作发生在程序里IDE预期我们使用A类型但是实际上我们使用B类型的时候,B类型的对象会自动转换为A类型的,如果没法转换,程序就会报错。赋值操作中就可能发生这样的情况。
我们先看赋值操作里表达式里面发生的自动转换,赋值操作A=B中,等号左边的A被叫做左值,B被叫做右值,程序期待事情是你给定的右值和左值类型完全相同。如果不相同,这里就会发生强制的类型转换,即把B的类型转化为A的类型。如果把一个超出左值类型表达范围的数赋值给左值,左值又是一个无符号类型,比如unsigned char c=-1;
这时-1(整型,负的),右值会转化为无符号字符型,初始值对无符号类型表示数值总数取模,然后求余数,这个余数就是转化后的数。
因为C++没有明确规定有符号类型的数应该如何表示,因此如果把一个超出左值类型表达范围的数赋值给左值,左值又是一个有符号类型,这种行为的结果是不一定的,因为C++标准委员会没有规定这样做之后到底会发生什么,因此各个IDE可能会有不同的实现。我们把这种不确定造成结果的行为叫做未定义行为。
建议:避免无法预知和依赖于环境的行为。
提示:切勿混用带符号类型和无符号类型。
转义序列(P36,2.1.3)
字符的转义序列可以为\
后面加上最多3个8进制数字(如果多于3个不会引发报错,多出的部分会被当成字符),或者\x
后面加上最多两个16进制数字(多出会报错)。数字转换成10进制后的大小不得超过字符集的限定范围。一般的字面值转义无此限制,不过,一般的字面值的类型是不确定的。10进制数字类型字面值会被转换为能够容纳这个数的带符号整数类型,其他进制中它们则会被转换为能容纳它们的占内存最小的类型的值。
在最新的C++14标准中,数字字面值里还允许以0b或者0B开头,后面加上二进制数成为二进制字面值。如0B101,代表数字5;0b11,代表数字3。
指定字面值的常量:当使用一个长整型字面值时,请使用大写字母L
来标记,因为小写字母l
和数字1
太容易混淆了。
变量(P38)
列表初始化
C++11标准:列表初始化(list initialization),用一组花括号来初始化变量。下面的第三种:
变量声明和定义的关系
变量能且只能被定义一次,但是可以被多次声明。声明变量:在变量名前添加关键字extern
,而且不要显示地初始化。
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
补:
extern外部变量声明其实是在IDE进行编译的时候告诉IDE,这有一个外部变量你要去别的地方找。因此我们应该掌握编译链接这套流程才能够更加方便的会用extern。假设有一个头文件a.h,这个头文件里面定义了int aaa=0;还有一个源文件b.cpp。这个b.cpp里面使用了extern int aaa;这样的语句,那么这个b.cpp是编译不了的。因为头文件如果不被别的源文件引用,是不参与被编译为obj的过程的,一旦它不参与这个过程,它里面声明的aaa这个全局变量其实就不存在,因此在b.cpp里面外部生命一个不存在的变量自然就是非法的。另外,使用extern也要和static做区分并考量它在别的文件中会不会造成内存污染等问题。这里应该掌握分离式编译的编译和链接特性再使用extern比较好。
静态类型(P42)
指针与引用
初始化所有指针: 初始化为nullptr或0。
void *指针(P50):特殊的指针类型,可以存放任意对象的地址。我们对该指针中到底是个什么类型的对象并不了解。
|
|
其中,p1是指向int的指针,p2是int。(强调变量具有的复合类型。)
指向指针的指针:通过*
的个数可以区分指针的级别,即:**
表示指向指针的指针,***
表示指向指针的指针的指针。
指向指针的引用(P52):
面对一条比较复杂的指针或者引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。
离变量名最近的符号(此例中是&r
的符号&
)对变量的类型有最直接的影响,因此r
是一个引用。声明符的其余部分用以确定r
引用的类型是是什么,此例中的符号*
说明r
引用的是一个指针。最后,声明的基本数据类型部分指出r
引用的是一个int
指针。
顶层const(P57,2.4.3)
顶层const是对const而言的,“顶层”可以用来修饰const
状态的形容词。一个const
使对象本身的值固定,这个const
就被称为顶层const;一个const
是对象指向或引用的对象成为固定值,这个const就被称为底层const。
顶层和底层const对拷贝来说密切相关,有相同底层const资格的两个对象才能够互相拷贝,而且顶层const声明变量之后不允许再次改变const的值。int p,const int *a=&p;
这种语句中的const就是底层const。
像int v1=9;const int *p=&v1;int *p2=p;
这种语句如果能够通过编译,那么我们就可以使用p2的性质改变p1指向的常量的值,但是常量的值是不能够被改变的,因此这种变相改变常量的值的表达式都是错误的。可以通过分析const级别得到表达式中常量是否被更改,从而判断语句的正确性。
说到底,顶层底层说的是对拷贝控制的约束。总的规则就是“不能改变常量的值”。因此“拷入和拷出的对象都要有相同的底层const资格,或者两个对象数据类型必须能转换”,例如,有int *p1,const int *p2;
。p1没有底层const,p2有底层const。p1=p2;这时const int*
不能转换成int *
(如果转换,就违反了“不能改变常量的值这一约束条件”),因此p1=p2;
不合法。p2=p1;``int *
能够转换成const int *
,因此p2=p1
合法。
constexpr(P58,2.4.4)
我们在了解constexpr
之前,应该先了解常量表达式。所谓常量就是固定的量,那么常量表达式就是值固定不变的表达式,这里“值固定不变”,指的是程序编译阶段,常量表达式的值就能被确定下来之后也不能对其进行任何种方式的修改。因此这个固定,是编译之后固定的。像cout<<1234<<endl;
中的1234,就是常量表达式,显然,字面值是常量表达式。
constexpr
的作用之一就是帮助程序员在IDE的提示下查看一个赋值语句是不是常量表达式。使用的方式包含在声明语句里面,形如constexpr 变量类型 变量名=右值;
如果右值是一个常量,这条语句就是正确的。在所有函数体外声明的全局变量的地址就符合“在编译期间能确定,编译后值不被改变”这两个条件,因此也属于常量。
另外,用constexpr声明的指针(比如,constexpr int *p=&v1;
中的*p
,相当于int *const p=&v1;
)都是顶层const,即指针本身值固定。但是指针指向的内容是可以变的。引用也一样。
定义于函数体之外的变量的地址是固定的,可以用来初始化constexpr指针。
当你使用constexpr定义引用变量的时候,这个变量引用的对象只能是全局基本数据类型(引用类型除外的)变量(因为要求的内存地址必须是固定不变的)。constexpr引用的结果和正常的引用的结果是一致的,因为引用本身就是固定不变的,因此相当于顶层const修饰的constexpr对引用类型类说没有特殊的意义。
类型别名(P61,2.4.4)
使用typedef int zhengxing;
这种对简单的类型名进行替换的方式无疑是非常直观并且好理解的,但是在涉及到复杂的类型名的时候往往会出现各种各样的问题。
比如typedef char *Pstring;
这条语句是不是就意味着我们看到Pstring
就可以用char *
替换呢。其实并不是,实际上类型别名不只是替换的规则,而是要复杂很多。
比如我们遇到const Pstring a;
的时候,按照替换的规则,这条语句就相当于const char * a;
这里的const这种情况下是底层const,但是结果并不是这样的,这条语句正确的等同语句应该是char *const a;
是一个顶层const,即指针本身是一个常量。让我们来分析一下为什么是这个样子,而不是简单的替换就行了。typedef char *Pstring;
这条语句就是说Pstring
是一个类型别名,它是什么类型的类型别名呢?Pstring是 指向char的指针的类型别名,也就是说,这个类型修饰的对象必须是一个指针,这个指针也必须指向char而不能指向别的什么东西,比如,不能指向const char
。我们再看看const Pstring a;
这个语句,首先a一定是指向char的指针。所以这个前面的const应该是用来修饰这个指针本身。也就是说,这个指针是常量指针而非指向常量的指针。这一点非常重要。
const char * a
这个语句里面,实际上类型是const char
,*
是声明符的一部分。我们说过,定义一个变量由两部分组成,类型名和声明符,声明符可以是*
或者&
加上变量名的形式。而类型别名只是给类型一个别名,至于声明符是怎样的,不在它修饰的范围内。因此在有const的情况下,就可以看出来这两者之间的区别还是很明显的。
|
|
pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针。
数据类型就变成了char
,*
成为了声明符的一部分。这样改写的结果是,const char
成了基本数据类型。cstr是一个指针,指向了常量字符。
auto类型声明符(P61,2.5.2)
C++11中引入的auto主要有两种用途:自动类型推断和返回值占位。auto在C++98中的标识临时变量的语义,由于使用极少且多余,在C++11中已被删除。前后两个标准的auto,完全是两个概念。
auto
变量通过初始化语句,计算出右值的类型,并推导出左值的类型。
这个过程中auto将会忽视顶层const和引用类型,可用const auto &a=i;
这种方式显式地指出了:指出要推导的结果是带顶层指针属性的或者是引用属性的。
auto推导多个值时,这些值的类型必须是一样的。因为auto是利用初始化赋值,因此它的行为基本上也和初始化有关。
关于auto的更多用法:【C++11】新特性——auto的使用
decltype类型指示符(P62,2.5.3)
有时候会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为此,C++11新标准引入第二种类型说明符decltype
。
decltype
不通过计算,只通过推算出变量应有的值,表达式本身应有的值和函数的返回值来推导类型。
对于变量类型,decltype
保留顶层const和引用的属性。对于表达式,解引用表达式(如:int i=1; int *p=&i; decltype (*p) a=i;
中的*p
,对p解引用是int &
类型的)和带括号的表达式,(如:decltype ((a+1)) c=i;
)的结果都将是引用类型。因为decltype通过处理表达式得到结果,因此更详细的内容在第四章将会被提到。有的表达式返回左值,有的表达式返回右值,返回左值的表达式在decltype类型推导下得到的将是引用的结果。
decltype((variable)) (注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。
补:
一般情况下,出现数组名的表达式时会把数组名转换为指针,而用decltype一个数组名时,其返回类型是该数组的类型,如有int ia[10],则
decltype(ia) da
,此时da也为包含10个int元素的数组。用于函数时也一样,不会自动把函数名转换为指针,而是返回该函数类型。如果作用于一个取地址运算符,则为指向指针的指针,如有int p,则
decltype(&p)
的结果是int **
类型。
用关键字struct自定义数据结构
使用struct关键字定义类的形式如struct 类名{数据成员类型1 数据成员名1;数据成员类型2 数据成员名2;};
,C++11规定可以给类内成员提供类内初始值用于初始化用我们自定义类创建的对象实例中的成员的值。形式如下:
很多新手程序员忘记在类定义的最后加上分号。
头文件保护符
|
|
头文件保护符依赖于预处理变量。如#define DEBUG
,此时DEBUG
就是预处理器变量。预处理变量无视C++语言中关于作用域的规则。
头文件保护符很简单, 程序员只要习惯性加上就可以了,没必要太在乎你的程序到底需不需要。
术语表
1.算数类型 arithmetic type
2.整型 integral type
3.转换 convert
4.不可打印 nonprintable
5.转义序列 escape sequence
6.类型说明符 type specifier
7.分离式编译 separate compilation
8.声明 declaration
9.声明符 declarator
10.静态类型 statically typed
11.类型检查 type checking
12.标识符 identifier
13.内层作用域 inner scope
14.复合类型 compound type
15.左值引用 lvalue reference
16.预处理 preprocessor
17.临时量 temporary
18.指向常量的指针 pointer to const
19.常量指针 const pointer
20.字面值类型 literal type
21.类型别名 type alias
22.类内初始值 in-class initializer
23.预处理器 preprocessor
24.头文件保护符 header guard
参考资料
- C++ Primer 中文版 第5版
- https://zhuanlan.zhihu.com/p/21820756