Link
❶
C++是种编译型语言。要运行一个程序,其源文本需要通过编译器处理, 生成一些目标文件,再经链接器组合后给出一个可执行文件。
ISO C++ 标准定义了两类东西:
- 语言核心特性, 诸如内置的类型(例如 char 和 int) 以及循环(例如 for-语句 和 while-语句)
- 标准库组件, 诸如容器(例如 vector 和 map) 以及 I/O 操作(例如 << 和 getline())。标准库组件完全是普通的 C++ 代码,由具体的 C++ 实现提供。 换句话说, C++ 标准库可以且确实是用 C++ 自身 (包括少量的机器语言代码,用于线程上下文切换等功能)实现的。
C++ 是静态类型语言。 就是说,任何一个东西(例如对象、值、名称和表达式)被用到的时候, 编译器都【必须】已经知晓其类型。对象的类型确定了可施加操作的集合。
❷
如果在 C++ 程序里要做一件事,主要的方式是调用某个函数去执行它。 定义函数就是指定某个操作怎样被执行。 除非事先声明过,否则函数无法被调用。
函数声明给出了该函数的名称、返回值类型(如果有的话)、 以及调用它时必须提供的参数数量和类型。
函数声明中可以包含参数名。 这对程序的读者有益,但除非该声明同时也是函数定义,编译器将忽略这些参数名。例如下面两种声明实质是一样的:
double sqrt(double d); // 返回 d 的平方根
double square(double); // 返回参数的平方
函数可以作为类的成员。 对于成员函数(member function)来说,类名也是该函数类型的组成部分。例如:
char& String::operator[](int index);
❸
声明是把一个实体引入程序的语句。它规定了这个实体的类型:
- 类型(type) 规定了一组可能的值和一组(针对对象的)运算;
- 对象(object) 是一块内存,其中承载某种类型的值;
- 值(value) 是一些二进制位,其含义由某个类型规定;
- 变量(variable) 是一个具名对象。
❹
C++ 有多种初始化方法,比如上面用到的 =
, 还有一种通用形式,基于花括号内被隔开的初值列表:
double d1 = 2.3;// d1 初始化为 2.3
double d2 {2.3};// d2 初始化为 2.3
double d3 = {2.3};// d3 初始化为 2.3(使用 { ... } 时,此处的 = 可有可无)
complex<double> z = 1;// 一个复数,使用双精度浮点数作为标量
complex<double> z2 {d1,d2};
complex<double> z3 = {d1,d2};// (使用 { ... } 时,此处的 = 可有可无)
vector<int> v {1,2,3,4,5,6};// 一个承载 int 的 vector
没有特定原因去指明类型时,就可以用auto
,“特定原因”包括:
- 如果该定义处在较大的作用域中,希望其类型对阅读源码读的人一目了然。
- 希望明确规定变量的取值范围或精度(比方说,想用double,而非float)。
❺
关于不可变更,C++有两种概念:
-
const
:相当于“我保证不会修改这个值”。 它主要用于指定接口,对于通过指针以及引用传入函数的数据,无需担心其被修改。 编译器为const
作出的“保证”担保。一个const
的值可在运行期间得出。 -
constexpr
:相当于“将在编译期估值”。 它主要用于定义常量,指定该数据被置于只读内存(在这里被损坏的几率极低)中, 并且在性能方面有益。constexpr
的值必须由编译器算出。
constexpr double square(double x) { return x*x; }
int var = 17;
constexpr double max1 = 1.4*square(17);// OK 1.4*square(17) 是常量表达式,可在【编译期】估值
constexpr double max2 = 1.4*square(var);// 报错:var不是常量表达式
const double max3 = 1.4*square(var);// OK,可在【运行时】估值
要成为constexpr
,函数必须极其简单,且不能有副作用,且只能以传入的数据作为参数。 尤其是,它不能修改非局部变量,但里面可以有循环,以及它自己的局部变量。例如:
constexpr double nth(double x, int n) {// 假定 n>=0
double res = 1;
int i = 0;
while (i<n) {
res*=x;
++i;
}
return res;
}
在某些场合下,语言规则强制要求使用常量表达式(比如:数组界限、case标签、模板的值参数,以及用constexpr定义的常量)。其它情况下,编译期估值都侧重于性能方面。 抛开性能问题不谈,不变性(状态不可变更的对象)是一个重要的设计考量。
❻
char v[6]; // 6个字符的数组
char* p; // 指向字符的指针
在声明里,[]
的意思是“什么什么的数组”,而*
的意思是“指向什么什么东西”。
char* p = &v[3]; // p指向v的第四个元素
char x = *p; // *p是p指向的对象
以上,v有六个元素,从v[0]到v[5]。指针变量p可持有相应类型对象的地址。
在表达式里,一元前置运算符*
的意思是“什么什么的内容”, 而一元前置运算符&
的意思是“什么什么的地址”。我们可以把前面初始化定义的结果图示如下:
如下示例是将数组v中的所有元素加一,若不想把v中的值复制到变量x,而是仅让x引用一个元素:
void increment() {
int v[] = {0,1,2,3,4,5,6,7,8,9};
for (auto& x : v)// 为v里的每个x加1
++x;
}
在声明中,一元前置运算符&
的意思是“引用到什么什么”。 引用和指针类似,只是在访问引用指向的值时,无需前缀*
。 此外,在初始化之后,引用无法再指向另一个对象。
在定义函数参数时,引用就特别有价值。例如:
void sort(vector<double>& v);
通过引用,我们确保了在调用sort(my_vec)的时候,不会复制my_vec, 并且被排序的确实是my_vec,而非其副本。
想要不修改参数,同时还避免复制的开销,可以用const
引用。接收const
引用参数的函数很常见。
运算符(例如&
、*
及[]
)用在声明中的时候, 被称为声明运算符(declarator operator):
T a[n] // T[n]: 具有n个T的数组
T* p // T*: p是指向T的指针
T& r // T&: r是指向T的引用
T f(A) // T(A): f是个函数,接收一个A类型的参数,返回T类型的结果
在老式代码里,通常用0
或NULL
,而非nullptr
。 但是,采用nullptr, 可以消除整数(比如0或NULL)和指针(比如nullptr)之间的混淆。
对指针指的判定(比如if(p)),等同于将其与nullptr比较(也就是if(p!=nullptr))。
❼
初始化和赋值不一样。 一般来说,想要让赋值操作正确运行,被赋值对象必须已经有一个值。 另一边,初始化的任务是让一块未初始化过的内存成为一个有效的对象。 对绝大多数类型来说,针对 未初始化变量 的读取和写入都是未定义的(undefined)。 对于内置类型,这在引用身上尤其明显:
int x = 7;
int& r {x}; // 把r绑定到x上(r引用向x)
r = 7; // 不论r引用向什么,给它赋值
int& r2; // 报错:未初始化引用
r2 = 99; // 不论r2引用向什么,给它赋值
很幸运,不存在未初始化的引用; 如果能,那么r2=99就会把99赋值给某个不确定的内存位置; 其结果会导致故障或者崩溃。
=
可用于初始化引用,但千万别被它搞糊涂了。例如:
int& r = x; // 把r绑定到x上(r引用向x)
这依然是初始化r,并把它绑定到x上,而不涉及任何的值复制操作。
初始化和赋值的区别,对很多用户定义的类型 ——比如string和vector——而言同样极度重要, 在这些类型中,被赋值的对象拥有一份资源,该资源最终将被释放。
参数传递和返回值返回的基本语义是初始化。举例来说,传引用(pass-by-reference)就是这么实现的。
忠告
- [1] 别慌!船到桥头自然直;
- [2] 不要专门或单独使用内置特性。 恰恰相反,基本(内置)特性,最好借助程序库间接使用, 比方说 ISO C++ 标准库(第8-15章);
- [3] 想写出好程序,不必对C++掌握到巨细靡遗。
- [4] 把力气用在编程技术上,别死磕语言特性。
- [5] 有关语言定义相关问题的最终解释, 请参考 ISO C++ 标准;
- [6] 把有用的操作“打包”成函数,再取个好名字;
- [7] 函数应当仅具有单一的逻辑功能;
- [8] 保持函数简短;
- [9] 当函数针对不同类型执行同样概念的操作时,请采用重载;
- [10] 当函数可能在编译期估值时,用
constexpr
声明它; - [11] 去理解基本语义向硬件的映射;
- [12] 用数字分隔符为大文本值提高可读性;
- [13] 不要使用复杂表达式;
- [14] 不要使用导致范围缩小的类型转换;
- [15] 尽量让变量的作用域保持最小;
- [16] 不要使用“魔数”;使用符号常量;
- [17] 尽量用不可变更的数据;
- [18] 每个声明里有(且仅有)一个名称;
- [19] 保持常见和局部名称简短,让不常见和非局部名称长一些;
- [20] 不要使用形似的名称;
- [21] 不要使用全大写(
ALL_CAPS
)名称; - [22] 在提及类型的声明里,尽量用
{}
-初始化 语法; - [23] 使用
auto
以避免重复输入类型名; - [24] 尽量别弄出来未初始化的变量;
- [25] 尽量缩小作用域;
- [26] 如果在
if
-语句的条件中定义变量,尽量采用针对0
的隐式判定; - [27] 仅在涉及位操作时,使用
unsigned
; - [28] 确保对指针的使用简单且直白;
- [29] 用
nullptr
,而非0
或NULL
; - [30] 在有值去初始化它之前,别声明变量;
- [31] 别给直观的代码写注释;
- [32] 用注释阐释意图;
- [33] 保持缩进风格一致。
网友评论