抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Hello World

人よ、幸福に生きろ!

基础

程序

  1. ISO C++标准中定义了两种实体:
  • 核心语言特性.i.e.内置类型(char,int).循环(for,while)
  • 标准库组件.i.e.容器(vector,map).输入输出操作(<<,getline)
  1. C++是一种静态语言。这意味着,每一个实体(对象、值、名称和表达式等)被使用的那一刻,编译器必须知道其准确的类型。
1
int main(){}  //最小的cpp程序
1
2
3
4
5
6
//import std; 是cpp20的特性
#include<iostream> //标准库组件

int main(){
std::cout<<"Hello, World!\n"; //标准库命名空间std::
}

Hello, World!

函数

1
2
3
double sqrt(double d);  //函数声明的时候可以包含参数的名称
double s = sqrt(2); //检测参数类型在必要的时候会发生隐式类型转换
char& String::operator[](int index); //函数的类型是char& String::(int),函数可以是类成员

类型、变量和运算

  1. unsigned一般用于位运算
  2. 数字可以是浮点数或者整数:
    • 浮点字面量含有小数点或者指数符号
    • 0b二进制整数 //0b0101010
      0x十六进制整数 //0x3f3f3f3f
      0八进制整数 //0334
  3. 使用单引号(’)作位数字分隔符

0x3.243F’6A88’85A3’08D3

  1. 初始化:
    • = 的形式是C语言传统的形式,会发生隐式类型转换
    • {} 更通用,而且可以省略’=’,不会发生隐式类型转换
    • 常量被声明时必须被初始化,用户自定义类型可以在定义时被隐式初始化
    • 如果变量的类型可以从初始化符号中推导出来,就无需显式指定类型
    • 使用 auto 常用在泛型编程中,以避免书写冗长的类型名称及重复代码
1
2
3
4
5
6
7
double d1 = 2.3;
double d2 {2.3};
double 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};
1
2
auto y = 3.14;
auto z {sqrt(y)};

作用域和生命周期

  1. 声明语句把一个名字引入作用域:
    • 局部作用域:在函数/匿名函数中定义的名字叫做局部名字(包括函数参数的名字),以语句块({})结尾.
    • 类作用域:不在函数、匿名函数或 enum class 中但在类的内部.
    • 命名空间作用域:不是局部名字和类名字且在命名空间的内部.
  2. 生命周期:
    • 对象必须先被构造(初始化)才能被使用,并且在退出作用域时被销毁。
    • 命名空间对象在程序结束时被销毁。
    • 对象成员的销毁时间点取决于所属对象的销毁时间点。
    • 一个用new创建的对象则可以持续生存,直到用delete将其销毁。

常量

  • const

    用来说明接口,可以用指针或者引用的方式传入参数而不用担心被改变
    编译器负责强制执行 const 承诺
    const声明的值可以在运行时计算

  • constexpr

    声明常量,在只读内存中,提高性能
    constexpr 的值必须由编译器运算

const double s1 = sum(v); //可行:允许在运行时运算

constexpr double s2 = sum(v);

1
2
3
4
5
6
7
8
9
double sum1(const double&);
double sum2(const vector<double>&);

vector<double> v {1.2, 3.4, 5.6};

const double s1 = sum1(1.0); //可行
constexpr double s2 = sum1(1.0); //不行
constexpr double s3 = sum2(v); //不行
}
  • 若要使函数在常量表达式(上文代码)中使用,这个函数必须被定义为 constexprconsteval
  • constexpr 函数
    • 在输入非常量参数时,输出的不是常量表达式
    • 在输入常量参数时,输出的才是常量表达式
  • consteval 函数可以让输入变量只能为常量表达式,即可以省略输入的常量表达式语法,还可以使输出为常量表达式。
1
2
3
4
5
constexpr double squre(double x){return x*x;}

constexpr double max1 = 1.4*squre(17) //可行,17为常量
constexpr double max2 = 1.4*squre(var) //错误,var不是变量,所以返回值不是常量表达式
const double max3 = 1.4*squre(var) //可行
1
2
3
4
consteval double square2(double x){return x*x;}

constexpr double max1 = 1.4*square2(17); //可行
const double max2 = 1.4*square2(var); //错误,函数输入只能是常量表达式

总结

这里讨论的有三个部分,函数的返回值类型(函数的声明),函数的输入参数类型,数据类型,均有两种写法,注意分辨

函数的输入参数类型

  • (数据类型 变量名):可以是变量,也可以是常量表达式
  • (const 数据类型& 变量名):函数不会修改他它的参数

数据类型

  • const:说明接口,意为“只读”
  • cosntexpr:声明常量,赋值给他的只能是常量表达式

函数的返回值类型

  • constexpr:不指定输入类型,输入和输出的不变性一致
  • consteval:输入和输出均为常量表达式

注意const只具有接口性质,不是实际上的常量

函数的声明为常量(constexpr,consteval)可以实现C++的纯函数,即数学意义上的函数,不会有副作用,不能修改函数的输入参数

指针、数组和引用

1
2
3
4
5
6
T a[n];  //数组
T* p; //指针
T& r; //引用

char* p = &v[3]; //p指向数组v的第四个元素
char x = *p; //*p代表p指向的对象(*解引用)

注意*在定义时是定义指针,在指针前使用是解引用

空指针

空指针用nullptr表达,整数用0或NULL表达,在检验时是兼容的

不能引用空指针

映射到硬件

  • 赋值:
  1. p和q同时指向y的地址
1
2
3
4
int y = 3;
int* p = &x;
int* q = &y;
p = q;
  1. 地址r1,r2不同,但是使不同的地址具有相同的值
1
2
3
4
5
int x = 2;
int y = 3;
int& r1 = x;
int& r2 = y;
r1 = r2;

这里直接用引用的方法操作地址,没有使用指针

  • 初始化:没有未经初始化的引用

使用 = 来初始化一个引用,注意这不是赋值

int& r = x;  //r指代x

补充在函数中引用的两种方法

void sort(vector<double>&v)

若只想减少复制参数的开销,则:

double sum(const vector<double>&)

这样就不能改变函数传入的参数

用户自定义类型

  • 内置类型:用基本类型、const操作符和声明操作符构造出来的类型
  • 用户自定义类型:利用C++的抽象机制从其他类型构造出来的类型,包括类和枚举类型

结构 struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Vector {
double* elem;
int sz;
};

void vector_init(Vector& v, int s){
v.elem = new double[s];
v.sz = s;
}

double read_and_sum(int s)
//从cin中读入s个整数,然后返回它们的和;假定s为正
{
Vector v;
vector_init(v,s);

for(int i=0;i!=s;i++){
std::cin>>v.elem[i];
}

double sum = 0;
for(int i=0;i!=s;i++){
sum += v.elem[i];
}
return sum;
}

访问结构体的成员有两种方式

  • . 表示通过名字或者引用访问
  • -> 表示通过指针访问
1
2
3
4
5
void f(Vector v, Vector& rv, Vector* pv){
int i1 = v.sz; //通过名字访问
int i2 = rv.sz; //通过引用访问
int i3 = pv->sz; //通过指针访问
}

类 class

把类型的接口(所有代码都可使用的部分)与其实现(可访问外部不可访问的数据)分离开来的语言机制被称为

1
2
3
4
5
6
7
8
9
class Vector {
public:
Vector(int s) :elem{ new double[s] }, sz{ s } {} //构造函数
double& operator[](int i) { return elem[i]; }
int size() { return sz; }
private:
double* elem;
int sz;
};
  • 构造函数在初始化类对象时一定会被调用
  • 错误处理暂时没有涉及
  • 思考通过new获取的double数组如何归还?

structclass没有本质区别,struct是默认public的

枚举

enum class

  • 枚举类型用于表示少量整数数值的集合
  • 通过符号名称替代整数
  • 后面的class表示这个枚举类型是强类型,并且具备独立作用域

不能混用不同类的枚举值
无法隐式地混用枚举类型与整数类型的值

  • 默认情况下,一个enum class定义仅定义赋值操作符、初始化函数及比较操作符,也可以自定义其他操作符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum class Color { red, blue, green };
enum class Traffic_light { green, yellow, red };

Color col = Color::red;
Traffic_light light = Traffic_light::red;

Traffic_light* operator++(Traffic_light& t) {
using enum Traffic_light;

switch (t) {
case green:return t = yellow;
case red:return t = green;
case green:return t = red;
}
}

Color x = red
Color y = traffic_light::red

int i = Color::red
Color c = 2

Color z = Color::red
auto z = Color::red

Color x = Color{5}
Color y {6}

enum

  • 普通enum中的枚举值进入与enum自身同级的作用域
  • 可以被隐式转换为整数数值

用枚举表示一组命名的常量
为枚举定义操作来简化并保证安全
优先使用enum class以避免很多麻烦

1
2
enum Color {red, green, blue};
int col = red; //col {0}

联合 union

  • 实际占用空间就是它最大成员所占的空间,可以用来节约空间
  • 同一时刻,union中只能保存一个成员的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum class Type { ptr, num };

union Value {
Node* p;
int i;
};

struct Entry {
string name;
Type t;
Value v;
};

void f(Entry* pe)
{
if (pe->t == Type::num)
cout << pe->v.i;
//...
}

避免使用“裸”union;将其与类型字段一起封装在一个类中

使用标准库类型variant可以消除大多数需要直接使用union
variant更简单,安全

1
2
3
4
5
6
7
8
9
10
11
struct Entry {
string name;
variant<Node*,int> v;
};

void f(Entry* pe)
{
if (holds_alternative<int>(pe->v))
cout << get<int>(pe->v);
//...
}

模块化

一个C++程序包含许多独立开发的部分,例如函数、用户自定义类型、类层次、 模板
为了让这些独立开发的部分在其他地方使用(类似于函数的声明),我们使用模块化的方法

区分声明(用作接口)和定义(用作实现)

分离编译

头文件

  • 在C++中,接口文件后缀名为.h,里面应该包括函数的声明
  • 实现接口函数的文件后缀名为.cpp,为了帮助编译器保持一致性,同样应该包含提供接口的.h文件
  • 要使用这个头文件的函数,就需要在头文件中包含"xxx.h"

使用#include及头文件实现模块化是一种传统方法,它具有明显的缺点。

  • 编译时间:不同的文件每一次include不同的头文件编译器都要处理一次
  • 依赖顺序:先include的头文件的定义与宏可能会影响后一个的代码的含义
  • 不协调:可能会出现类似下面的问题
    1. 我们有意无意地在两个源文件中定义了同一个实体的时候
    2. 不同源文件引用头文件的顺序不一致的时候
  • 传染性:一个文件(1)包含的头文件(2)会包含其他这个文件(1)不需要的头文件,以此类推会导致代码膨胀且冗余

避免在头文件中定义非内联函数
不要在头文件中使用 using 指令

模块

  • 写法1:将class export并表示成声明的形式,就和.h写法差不多
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export module Vector;

export class Vector {
public:
Vector(int s);
double& operator[](int i);
int size();

private:
double* elem;
int sz;
};

Vector::Vector(int s) : elem{new double[s]}, sz{s} {}

double& Vector::operator[](int i) { return elem[i]; }

int Vector::size() { return sz; }

export bool operator==(const Vector& v1, const Vector& v2) {
if (v1.sieze() != v2.size()) return false;
for (int i = 0; i < v1.size(); i++)
if (v1.elem[i] != v2.elem[i]) return false;
return true;
}
  • 写法2:函数export的就是可见的,没有export的就是不可见的(类似java)

  • module Vector接口形式,后缀名为.cppm

  • import Vector使用Vector

  • export module Vector定义Vector

模块在维护性与编译时间方面的改进非常显著

命名空间

namespace 命名空间名 {}
:: 访问

  1. 表达某些声明是属于一个整体的
  2. 表明它们的名字不会与其他命名空间中的名字冲突

真正的main()(不是自己定义的命名空间里的)

using 声明将命名空间中的名字放进作用域,可以降低可读性

1
2
3
4
5
6
7
void my_code(vector<int>& x, vector<int>& y) {
using std::swap; // 将标准库的swap放进作用域
//...
swap(x, y); // std::swap()
other::swap(x, y); // 某个其他的swap()
//...
}

使用标准库命名空间中所有名称的访问权

using namespace std;

用以上方式使用命名空间指令不会影响使用模块的用户,影响仅限于模块内部

函数参数与返回值

函数之间传递信息有三种路径:

参数
全局变量
类对象中的共享状态

在函数的信息传递中,我们应考量:

  • 对象是被复制的还是共享的?
  • 这个共享对象是否可被修改?
  • 这个对象是否被移动,从而留下了一个空对象?

参数传递

  • 默认情况复制(传值)/直接指向引用(传引用)/直接只读const&
  • 拥有默认值的函数参数可以达到和重载函数一样的效果
1
2
3
void print(int value, int base = 10);
print(x,8);
print(x);//默认十进制
1
2
3
4
5
6
7
void print(int value, int base){
//...
}

void print(int value){
print(int value, 10);
}

返回值

  • 返回值的默认行为是复制
  • 返回引用的情况只应当出现在返回的内容不属于函数局部的时候(注意数组)
  • 局部变量在函数返回的时候消失,因此我们不应当返回它的引用或者指针
  • 对于较大的对象构造移动方法来极大地减少复制开销
  • 编译器会优化复制行为,叫做省略复制优化

使用指针返回大对象的代码性能不会比使用引用返回的函数好,不要使用这样的代码

1
2
3
4
5
6
7
8
9
10
11
Matrix* add(const Matrix& x, const Matrix& y) {
Matrix* p = new Matrix;
//....
return p;
}

Matrix m1, m2;
//...
Matrix* m3 = add(m1, m2); // 只复制指针
//...
delete m3; // 很容易忘记

返回类型推导

使用 auto 关键字

auto mul(int i, double d) { return i*d; }

返回类型后置

有的时候,我们需要先看到参数,然后决定返回值的类型。

这包括但不限于返回类型推导这种情形,这个问题与命名空间、匿名函数、概念都有一定的联系

使用 auto 关键字,表示返回值会在后面提到

auto mul(int i, double d) -> double { return i*d; }

使用这种记法能够更有效地实现代码对齐

auto next_elem -> Elem*;
auto exit(int) -> void;
auto sqrt(double) -> double;

不要过度使用返回类型推断

结构化绑定

把类对象成员赋予局部变量名称的机制

使用 {s, i} 构造
使用 [n, v] 读取(“解包”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Entry {
string name;
int value;
};

Entry read_entry(istream& is) // 简单地读函数
{
string s;
int i;
is >> s >> i;
return {s, i};
}

auto [n, v] = read_entry(cin);
cout<<n<<' '<<v<<'\n';

对完全没有私有数据的类使用结构化绑定时,绑定行为是明显的:
提供的名称数量必须与数据成员的数量相等,每一个绑定名称对应一个成员变量

1
2
3
4
5
6
7
8
9
10
map<string, int> m;
//...
for (const auto [key, value] : m) {
cout << key < ' ' << value << '\n';
}

auto incr(map<string, int>& m) -> void {
for (auto& [key, value])
value++;
}

结构化绑定也可以用于处理需要通过成员函数访问对象数据的类(?)

complex<double> z = {1,2};  
auto [re,im] = z+2;

不要过度使用结构化绑定

评论