第1章 基础
# Cpp之旅(学习笔记)第1章
# 第1章 基础
# 1.1 程序
Cpp是一门编译型语言。源代码必须交由编译器处理生成可执行文件,然后由链接器组装成可执行程序。
一个可执行文件通常是为一个特定的硬件与操作系统组合而制定的,换句话说,它在安卓设备与Windows个人电脑之间是不可移植的,因此,可移植性指的是源代码的可移植性,即源代码可以在多种系统中编译成功,然后运行。
最小的Cpp程序:
int main(){}
定义一个main函数,不接受任何参数,也不做任何事情。
# 1.2 新东西
//旧版本
#include <iostream>
int main() {
std::cout << "Hello, World!\n";
}
2
3
4
5
//Cpp20版本
import std;
int main() {
std::cout << "Hello, World!\n";
}
2
3
4
5
import std;
指示编译器去声明标准库变量的存在。如果没有这个声明,std::cout << "Hello, World!\n";
将没有意义。但是指令import将所有标准库放进一个单独的std模块还没有成为标准。
# 1.3 函数
double get(const vector<double>& vec, int index);
//函数类型是:
double(const vector<double>&,int)
2
3
对于成员函数来说,类的名称也是函数类型的一部分:
char& String::operator[](int index);
//函数类型是:
char& String::(int)
2
3
# 1.4 类型、变量、运算
- 前缀0b表示二进制整数字面量,如:0b10101010
- 前缀0x表示十六进制整数字面量,如:0xBAD12CE3
- 前缀0表示八进制字面量,如:0334
可以引入单引号(’)作为数字分隔符提升长字面量的可读性
例如:Π的值大约是:3.14159 ‘ 26535 ’ 89793 ‘ 23846 ’ 26433 ‘ 83279 ’ 50288
用十六进制表示就是:0x3.243F ’ 6A88 ‘ 85A3 ’ 08D3
# 1.4.1 运算
部分操作符的计算顺序是从左向右的:
x.y、x->y、x(y)、x[y]、x<<y、x>>y、x&&y、x||y
但赋值符号的计算顺序是从右往左的:
x += y
# 1.4.2 初始化
- 使用=或者{}初始化
double d1 = 2.3;
double d2 {2.3};//等价于double d2 = {2.3}
2
- 使用=的形式是C语言传统的方式,如果拿不定主意,就是用更通用的{}列表形式。可以避免隐式类型转换导致的信息丢失
int i1 = 7.8;//i1变成了7(你可能感到意外)
int i2 {7.8};//错误:floating-point to integer conversion
2
- 当使用=而不是{}的时候,会进行从double到int及从int到char这样的窄化类型转换。
如果变量的类型可以从初始化符号中推导出来,就无需显示指定类型
auto b = true; //bool类型
auto ch = 'x'; //char类型
auto i = 123; //int类型
auto d = 1.2; //double类型
...
2
3
4
5
使用auto声明变量时,作者倾向于使用=符号,因为没有类型转换的风险。当然,偏好使用{}也无伤大雅。
当没有明显的需要显示地指定类型时,一般使用 auto 。
理由如下:
- 该定义的作用域较大,我们希望代码的读者清楚地知道其类型。
- 初始化表达式的类型(对读者来说)不是显而易见的。
- 我们希望明确规定某个变量的范围和精度(如:希望使用double而非float)。
# 1.5 作用域和生命周期
- **局部作用域:**在函数或匿名函数中定义的名字叫局部名字,作用域从声明它的地方开始,直到声明语句所在的块结尾。语句块的边界由一对{}决定。函数参数的名字也属于局部名字。
- **类作用域:**定义在类的内部,不在任何函数、匿名函数、enum class中,可被叫做成员名字(或类成员名字)。作用域从它括起声明的左花括号 { 开始,到对应的右花括号 } 结束。
- 命名空间作用域:在命名空间内部,并且不再任何函数、匿名函数、enum class中,则把这个名字叫做命名空间成员名字。作用域从声明它的地方开始,到命名空间结束为止。
某些对象也可以没有名字,比如:临时对象或者用new创建的对象:
vector<int> vec; //vec是全局名字(全局整数动态数组)
void fct(int arg) { //fct是全局名字(全局函数)arg是局部名字(局部整数参数)
string motto {"Who dares wins"};//motto是局部名字
auto p = new Record{"Hume"}; //p指向无名Record对象(由new创建)
//...
}
struct Record {
string name; //name是Record的成员名字(字符串成员)
//...
};
2
3
4
5
6
7
8
9
10
一个new创建的对象可以持续“生存”,知道用delete将其销毁。
# 1.6 常量
Cpp支持两种不变性:
- const:“我承诺不修改这个值”,主要用来说明接口,可以用指针或者引用的方式传入函数参数而不用担心被改变。编译器负责强制执行const承诺。const声明的值可以在运行时被计算。
- constexpr:“请在编译时计算出它的值”,主要用于声明常量,作用是把数据置于只读内存区域(更小概率被破坏),以及提高性能。constexpr的值必须由编译器计算。
例如:
constexpr int dmv = 17; //dmv是一个命名常量
int var = 17; //var不是常量
const double sqv = sqrt(var); //sqv是一个命名常量,可能在运行时计算
double sum(const vector<double>&); //sum不会修改它的参数
vector<double> v {1.2, 3.4, 4.5}; //v不是常量
const double s1 = sum(v); //可行:sum(v)在运行时计算
constexpr double s2 = sum(v); //错误:sum(v)不是一个常量表达式
2
3
4
5
6
7
为了使函数可在常量表达式中使用,这个函数必须被定义为constexpr 或consteval,这样才能在编译期表达式中被计算。
例如:
constexpr double square(double x){return x*x};
constexpr double max1 = 1.4*square(17); //可行:1.4*square(17)是常量表达式
constexpr double max2 = 1.4*square(var);//错误:var不是常量,所以square(var)不是常量
const double max3 = 1.4*square(var); //可行:允许在运行时计算
2
3
4
# 1.7 指针、数组、引用
- 数组:同类型元素的连续分配序列。
- 指针:可存放指定类型的对象的地址。
在声明中,[ ]
意味着对应类型的数组,*
意味着指向对应类型的指针。
char v[6]; //6个字符组成的数组
char *p = &v[3];//p指向v的第4个元素
char x = *p; //*p代表p指向的对象
2
3
- 前置一元操作符
*
表示取内容,前置一元操作符&
表示取地址,后置一元操作符&
表示指向前者的引用。 - 引用在初始化后就不能再指向其他的对象了。
- 当用于什么语句时,操作符
&
、*
、[ ]
被称为声明操作符。
T* p //T*:p是一个指向T的指针
T& r //T&:r是一个指向T的引用
T f(A) //T f(A):f是一个函数,接受A类型的参数,返回T类型的结果
2
3
# 1.7.1 空指针
当确实没有对象可指向,我们希望表达出一种“没有对象可用”的含义时,可令指针取值为nullptr。所有指针类型共享同一个nullptr。
在使用指针前检查它是否为空,是明智的行为:
int count_x(const char* p, char x){
//计算x在p[]中出现的次数
//假定p指向一个以零结尾的字符数组(或者指向空)
if(p == nullptr)
return 0;
int count = 0;
for(; *p != 0; ++p)
if(*p == x)
++count;
return count;
}
2
3
4
5
6
7
8
9
10
11
- 假定输入的char*是一个C风格字符串,也就是说,改指针指向一个以零结尾的char数组。字符串字面量中的字符是不可变的。
- 所以为了以count_x("Hello!"),这样的格式接收字符串字面量作为参数,因此把第一个参数声明为const char*。
- 在旧式代码中,常常用0或者NULL代替nullptr,但是,使用nullptr可以消除整数(0或者NULL)与指针(nullptr)之间存在的潜在歧义。
- 指针可以为空,但引用不能,引用必须指向有效的对象(编译器也假定如此)。当然,有些奇淫巧计可以打破这个规则,但请不要这么左。
# 1.8 建议
- 使用#include 或者 import引入库,可以简化编程。
- 函数重载的适用情况是,几个函数的任务相同而处理的参数类型不同;
- 如果一个函数可能需要在编译时求值,那么将它声明为constexpr;
- 如果一个函数必须在编译时求值,那么将它声明为consteval;
- 如果一个函数不允许有副作用,那么就将它声明为constexpr或consteval;
- 在声明语句中制定了类型名称(而非使用auto)时,优先使用{}初始化语法;
- 使用auto可避免重复输入类型名称;
- 在if语句的条件中声明变量时,优先采用隐式检验而不是与0或nullptr进行比较;
- 优先使用范围for语句而非使用显示循环变量的传统for语句;
- 只对位运算使用unsigned;
- 使用nullptr而非0或NULL;