C++ primer ch2 - Variables and Basic Types 阅读笔记
最近看了下C++ roadmap,发现自己有些基础的内容还不是很懂,于是选了本经典的C++书来读,发现确实有很多基础知识没掌握。读完了第二章,边读边记了些之前不太懂和感觉重要的内容。
Primitive Built-in Types
Primitive types (fundamental types)
- arithmetic types
- void
https://en.cppreference.com/w/cpp/language/type
按照cppreference的说法,type就分fundamental和compound两大类,C++ primer这里说的Primitive types应该就是fundamental type的一部分。
Arithmetic Types
- integral types
- floating-point types
unsigned int
可以被简写成unsigned
。
The type unsigned int may be abbreviated as unsigned
Type Conversions
out-of-range assignment
If we assign an out-of-range value to an object of unsigned type, the result is the remainder of the value modulo the number of values the target type can hold.
关于modulo怎么算,看之前的一篇文章。
If we assign an out-of-range value to an object of signed type, the result is undefined.
literal
整数literal默认类型(比我想的复杂):
- decimal literals are signed
- decimal literal has the smallest type of int, long, or long long (i.e., the first type in this list) in which the literal’s value fits
- octal and hexadecimal literals can be either signed or unsigned
types
- Octal and hexadecimal literals have the smallest type of int, unsigned int, long, unsigned long, long long, or unsigned long long in which the literal’s value fits
负数literal
负数literal(字面值)的符号不包含在literal里: -42 符号是一个operator,作用在42上
Floating-point literal
浮点数literal默认的类型是double
字符串literal
字符串literal的type是常量字符数组 (array of constant chars)
字符串连接
两个字符串literal,中间用 空格、tab、换行 隔开的话,会被连接成单个字符串literal "a really, really long string literal " "that spans two lines" 效果等于 "a really, really long string literal that spans two lines"
Variables
C++ Primer 对于 object 的定义是: > An object is a region of memory that has a type. We will freely use the term object regardless of whether the object has built-in or class type, is named or unnamed, or can be read or written.
Initializer(初始值)
initialization and assignment are different operations in C++
C++里变量初始化和赋值是不同的操作
Initialization is not assignment. Initialization happens when a variable is given a value when it is created. Assignment obliterates an object’s current value and replaces that value with a new one.
初始化发生在创建一个变量的时候给这个变量初始值;赋值(assignment)是更改一个对象的值,用新值替换掉旧值。
定义变量但是不给初始值,那么会被default initialized
。
有built-in
type的变量,在函数之外被定义的时候如果没有给初始值,会被初始化为0。如果在函数之内被定义并没有给初始值,那么它是uninitialized,它的值是undefined。
自定义类型(类)的变量定义时如果没有给初始值,那初始化是由这个类来控制的。(我猜,定义这种变量不给初始值的前提,是这个类有不带参数的构造函数(constructor))。
建议:定义变量时就显式地给初始值。
List Initialization
C++11新增了List Initialization
,用花括号初始化。也就有了下面四种初始化方式。
1 | // C++ primer EN p43 |
Variable Declarations vs Definitions
Declaration:makes a name known to the program, also specifying the type
Definition: in addition to declaration, creates an entity associated with a name - allocates storage and may provide the variable with an initial value
definition(定义)不仅declare(声明)一个变量的名字和类型,还给这个变量分配内存,可能会给它一个初始值
变量可以被declare多次,可以在多个文件里被declare,但是只能被define一次,也当然只可能在一个文件里被define。
extern
通过extern
关键字只declare而不define 1
2extern int i; // declares but does not define i
int j; // declares and defines j
只能define一次,但是可以declare多次。下面这个例子在defa.cpp和main.cpp都define了a,会导致link error。
#include "a.h"
会把a.h里的内容替换到当前位置,所以在哪#include "a.h"就相当于在哪declare
a,在例子里也就是在defa.cpp和main.cpp都declare了a。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// file: a.h
extern int a;
// file: defa.cpp
int a = 1;
// file: main.cpp
int a; // link error!
int main()
{
std::cout << "a: " << a << "\n";
return 0;
}
如果把main.cpp中的定义a删去就没有问题了。 1
2
3
4
5
6
7
8
9
int main()
{
std::cout << "a: " << a << "\n";
return 0;
}
运行结果: 1
a: 1
用extern同时有初始值 = definition 1
extern double pi = 3.1416; // definition
但是,在函数里用extern声明变量并给初始值的话就是错误。
1
2
3void func() {
extern double pi = 3.1416; // compile error
}
Conventions for Variable Names
原来C++里面有一类名字是C++ Alternative Operator Names
,可以像python一样用and,or,not等等表示布尔运算。这些alternative
operator是给一些没有&、|这种符号的编码集用的。(很少见,之前根本不知道)
Compound Types
declaration更广泛的定义: > More generally, a declaration is a base type followed by a list of declarators. Each declarator names a variable and gives the variable a type that is related to the base type.
例:
1 | int i3 = 1024, &ri = i3; // i3 is an int; ri is a reference bound to i3 |
*
和&
是type
modifier,是declarator的一部分。type
modifier只作用在使用它的变量上,所以下面这个declaration,只有p1
是pointer。
1
int* p1, p2; // p1 is a pointer to int; p2 is an int
int*
是所有后面变量的type。
之前我自己是更喜欢把&
和*
跟base
type写在一起,看到这里之后决定还是改成跟变量名写在一起比较明白。
Reference
A reference is an alias, not an object. A reference is just another name for an already existing object.
reference和它bind的object的type必须匹配(比如double&
type不能bind一个int
)。
一个reference只是一个名字,一个已有的object的另一个名字,reference没有自己的内存空间。
reference只能绑定(bind)到object上,不能绑定到字面值(literal)或者一个表达式的结果。
Pointer
pointer和它所指向的object的type必须匹配(比如int*
type不能指向一个double
)。
Pointer如果定义在block scope(一般是函数体)里,跟之前的built-in type的variable一样,如果没有给初始值,那它也是uninitialized,它的值是undefined value。这样的Pointer是invalid。
建议:定义pointer的时候就初始化它
null pointer不指向任何object 1
int *p1 = nullptr; // equivalent to int *p1 = 0;
pointer可以直接用在condition里(如if(pointer)
),如果pointer为0(nullptr
)那就是false,其他为true。
两个pointer用==比较,如果他们指向相同的地址那就是true。
Invalid pointer用在condition里或者比较的行为是undefined。
Understanding Compound Type Declarations
因为reference不是object,所以pointer不能指向reference。但是pointer是object,所以reference可以bind到pointer。
1
2
3
4
5
6// C++ primer 英文版p58
int i = 42;
int *p; // p is a pointer to int
int *&r = p; // r is a reference to the pointer p
r = &i; // r refers to a pointer; assigning &i to r makes p point to i
*r = 0; // dereferencing r yields i, the object to which p points; changes i to 0
const
Qualifier
const object必须给初始值(被初始化)。 1
2
3
4// C++ primer EN p59
const int i = get_size(); // ok: initialized at run time
const int j = 42; // ok: initialized at compile time
const int k; // error: k is uninitialized const
const Objects Are Local to a File
the compiler will usually replace uses of the variable with its corresponding value during compilation
To share a const object among multiple files, you must define the variable as extern.
const
object默认只在当前文件可见。需要跨文件使用同一个object的话,要用extern
。
1
2
3
4
5// C++ primer EN p60
// file_1.cc defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_1.h
extern const int bufSize; // same bufSize as defined in file_1.cc
需要注意的是,const
与initializer
同时使用,只能在global scope
里,也就是file_1.cc
里的语句不能放在任何函数中。
file_1.h
里的语句是declaration,之前也提到过,一个变量是可以声明多次的,所以每在其他cpp文件里引用file_1.h
就相当于声明了一次。
其实不引用file_1.h
,直接在cpp文件里用extern const int bufSize;
也是一样的效果,但是就会造成相同的代码复制到多个地方,以后如果要改就容易出错。
Reference to const
reference的类型和它绑定的object的类型需要匹配,但是有两个例外,这里是第一个例外:reference
to
const
,可以用literal
、表达式和nonconst
object初始化,还能用可以转换成reference的类型的其他表达式初始化
we can initialize a reference to const from any expression that can be converted (§ 2.1.2, p. 35) to the type of the reference. In particular, we can bind a reference to const to a nonconst object, a literal, or a more general expression
1 | int i = 42; |
Reference to const可以绑定nonconst的object,但是不能更改object的值。
Pointers and const
pointer to const
pointer的类型需要和它指向的object的类型匹配,但有两个例外,这里是第一个。
我们可以用一个pointer to const
指向一个nonconst
object 1
2
3// C++ primer EN p62
double dval = 3.14; // dval is a double; its value can be changed
const double *cptr = &dval; // ok: but can’t change dval through cptrconst double *cptr
:cptr是一个pointer,指向double
const。
const pointer
不同于reference,pointer自己也是一个object,所以pointer本身也可以是const。
1
2
3
4
5// C++ primer EN p63
int errNumb = 0;
int *const curErr = &errNumb; // curErr will always point to errNumb
const double pi = 3.14159;
const double *const pip = π // pip is a const pointer to a const object
Top-Level const
这个概念之前根本没接触过。
top-level const indicates that an object itself is const. Top-level const can appear in any object type, i.e., one of the built-in arithmetic types, a class type, or a pointer type. Low-level const appears in the base type of compound types such as pointers or references.
1 | // C++ primer EN p64 |
如果pointer是const pointer,也就是指针指向的地址不能改变,这个const是top level;如果pointer指向的object是const,这个const是low level。
reference只有low level const,因为reference不可能改变绑定的object(也可以说reference本身就只能是const),const的区别也就只在于reference绑定的object是不是const。
When we copy an object, top-level consts are ignored:
1 | i = ci; // ok: copying the value of ci; top-level const in ci is ignored |
在复制object的时候,top level const被忽略,nonconst当然可以赋值const object的值,const初始化的时候也可以用nonconst的值。
constexpr
and
Constant Expressions
Constant expression
an expression whose value cannot change and that can be evaluated at compile time.
- A literal is a constant expression
- A const object that is initialized from a constant expression is also a constant expression
1 | const int max_files = 20; // max_files is a constant expression |
不懂为什么limit也是constant expression,两个constant expression加法运算也是constant expression吗?
constexpr
Variables
C++11,
用constexpr
声明变量,编译器会帮我们检查这个变量是不是constant
expression。
用constexpr
声明的variable,隐含着声明了它是const
variable。(variable一般翻译成变量,但是const表示不变,const
variable意义互相矛盾就很奇怪了,直接翻译成常量
)
用constexpr
声明的variable,必须用constant
expression初始化。
Literal Types
可以用constexpr
的类型被叫做literal types
。
目前literal types
:
- arithmetic type
- reference type
- pointer type
可以初始化constexpr
pointer的expression:
- nullptr
- literal 0
- an object that remains at a fixed address
函数里的variable没有固定地址,所以不能用来初始化constexpr
pointer。(static例外?)
constexpr
pointer
constexpr imposes a top-level const (§ 2.4.3, p. 63) on the objects it defines
1 | // C++ primer EN p67 |
constexpr
pointer一定是一个不能改变所指地址的pointer(const pointer)。
Dealing with Types
Type Alias
1 | // C++ primer EN p67-68 |
Pointers, const, and Type Aliases
1 | typedef char *pstring; |
需要注意: const pstring cstr
不等于
const char *cstr
a const that appears in the base type modifies the given type.
When we use pstring in a declaration, the base type of the declaration is a pointer type.
base type到底是什么?在chapter summary里给了定义: > type specifier, possibly qualified by const, that precedes the declarators in a declaration. The base type provides the common type on which the declarators in a declaration can build.
The auto
Type
Specifier
使用auto
创建变量必须有初始值。 1
2// the type of item is deduced from the type of the result of adding val1 and val2
auto item = val1 + val2; // item initialized to the result of val1 + val2
跟其他type
specifier一样,可以使用一次auto定义多个变量,但是每个变量类型必须一致。
1
2auto i = 0, *p = &i; // ok: i is int and p is a pointer to int
auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi
Compound Types, const, and auto
感觉auto碰上const,reference和pointer就变得有好复杂了
使用auto声明的变量的类型,不一定跟给它的初始值的类型一样。
Reference
使用reference作为初始值,auto推断出的类型是和reference绑定的类型相同。这意味着:
- 使用int &类型的变量作为初始值,auto推测的类型是int。
1
2int i = 0, &r = i;
auto a = r; // a is an int (r is an alias for i, which has type int)
Top-level consts
auto忽略top-level consts。这意味着: - 使用const
int类型的变量作为初始值,auto推测的类型是int。因为这里const是top-level。
- 使用const int* 类型的变量作为初始值,auto推测的类型是const
int*。这里const是修饰int,而不是pointer,所以是low-level const。
1
2
3
4
5const int ci = i, &cr = ci;
auto b = ci; // b is an int (top-level const in ci is dropped)
auto c = cr; // c is an int (cr is an alias for ci whose const is top-level)
auto d = &i; // d is an int* (& of an int object is int*)
auto e = &ci; // e is const int* (& of a const object is low-level const)
如果要得到top level const,需要显式声明const auto
。
1
const auto f = ci; // deduced type of ci is int; f has type const int
Reference with top-level consts
但是,如果用auto声明一个reference,那top level
const就不会被忽略了。这意味着: - 使用const
int类型的变量作为初始值,auto &推测的类型是const int&。
1
2
3auto &g = ci; // g is a const int& that is bound to ci
auto &h = 42; // error: we can’t bind a plain reference to a literal
const auto &j = 42; // ok: we can bind a const reference to a literal
auto后pointer declarator
在下面这种情况下加不加*
效果是一样的 1
2
3auto i = 0;
auto *p = &i; // p is a pointer to int
auto d = &i; // d is also a pointer to int
但是和const一起用就不一样了…… > https://stackoverflow.com/a/40867640
1 | int i; |
The decltype
Type
Specifier
decltype是一个type specifier,也就是用来声明、定义变量用的类型。但是它不需要像auto那样必须给初始值。
decltype通过给它的expression来判断类型,但并不会evaluate这个expression。(也就是说如果expression是个需要计算或者函数,decltype并不会执行这个计算或者调用函数,它只是通过expression分析计算或者函数将会返回什么类型)
1 | // C++ Primer EN p70 |
如果expression只是一个variable,decltype得到的就是这个variable的type,包括top-level const和reference。
1 | // C++ Primer EN p71 |
decltype
and
References
Generally speaking,
decltype
returns a reference type for expressions that yield objects that can stand on the left-hand side of the assignment
看下面的例子。因为我们可以给*p
赋值,而且赋值改变的是p指向的object,所以decltype得到一个reference也很合理。
(如果decltype得到的是int类型,那么相当于新定义了一个变量,对新变量赋值不会影响原来的变量,这和*p
的作用不符。)
1 | // EN p71 |
如果在variable周围加上括号,那decltype
得到的类型一定是reference。
1 | // EN p71 |
Defining Our Own Data Structures
Best Practices: > Headers should have guards, even if they aren’t (yet) included by another header.
所有的头文件都应该用guard。 1
2
3
4
5
6
7
8
9
10// C++ primer EN p77
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
一些感想
虽然有很多知识盲区,现在才补上,但是就算不知道不还是这样工作快两年了,只能说目前工作里要处理的都是些业务逻辑,也很少需要细究C++基础知识。就算碰到什么不懂的语法,以前有搜索引擎,现在有chatgpt,工作总是能做的。
只是感觉这样做这对自己的能力也没太大提升,所以还是得回头看书系统地学一学。
不过之前看过一些观点说“系统学习”属于是学生思维了,我感觉也没错。如果让我一开始就看这本C++ Primer我也看不进去,反倒是现在工作一段时间觉得知识上有漏洞,带着比较强目的性看反而收获颇多。而且毕竟对语言熟悉了,看起来也不那么费劲。
总的来说还是实践更重要些,最好的还是快速入门然后做些项目,之后再来系统学习收获可能更多。