Chireiden

地霊殿

地霊殿,充满幻想与希望的殿堂


夢も希望も無い、毎日がそんな生活だった。

Cpp 入门

C++ is a horrible language. It's made more horrible by the fact that a lot of substandard programmers use it, to the point where it's much much easier to generate total and utter crap with it.
Linus Torvalds

为了便于讲解,本文采用由浅及深的方式组织行文主线。

本文主要采用对比 c 和 cpp 差异的方式来进行。


头文件

几乎所有的 c 语言代码都需要头文件,cpp 和 c 的头文件对比是开始编程前必须的一步。

几乎所有的 c 代码中 #include 命令都可以直接复制到 cpp 中使用,但是这并不推荐

#include 是什么?

// module.txt
printf("%d\n", a);
a *= 3;
// main.c
##include <stdio.h>

int main()
{
	int a = 1, b = 2;
##include "module.txt"
    printf("%d %d\n", a, b);
    return 0;
}

编译执行 main.c 后,输出应该是

1

3 2

可见,#include 作用是将一段文本直接引入到代码中。

对头文件使用 #include 时也是如此。

头文件名称对比

语言———–———————–———–———–———–
cstdio.hstdlib.hmath.h\|windows.hstring.h
cppcstdiocstdlibcmathvectorwindows.hcstring

可见,对于常见的头文件,是将 c 头文件的 .h 去掉,在前面加上 c,如 cstdio.h  。

尤其注意 string.h 文件,cstring 和 string 完全不同。

// cstdio
...
##include <stdio.h>
...

IOSTREAM

##include <iostream>
using namespace std;
...
cin >> a;
cout << b;

上面这一段应该是大多数人最早接触的 cpp 代码。

大概讲解一下这一段内容中每行代码发生了什么。

#include <iostream> 引入 iostream 头文件

using namespace std; 使用 std 命名空间,命名空间如果有时间放在后面讲。反正也没人用得到不如不讲了。

关于 cin 和 cout,我们可以构造这样一个模型:

graph LR; input["input"]-->cin["cin"]; cin["cin"]-->variable["variable"]; variable["variable"]-->cout["cout"]; cout["cout"]-->output["output"]; style cin fill:#CFC,stroke:#333,stroke-width:0.5px style cout fill:#CFC,stroke:#333,stroke-width:0.5px style variable fill:#FFC,stroke:#333,stroke-width:0.5px

cin 和 cout 仅仅是一个管道,用来让数据从中流过(data stream),这就是所谓的 iostream(input & output stream)。

cin 可以用来输入多个值,如 cin >> a >> b;

cin 可以隐式转换为 bool

cout 用法同理。


新的关键字

本篇只介绍 newdelete

简单介绍malloc 系列

malloc 及其内存分配系列函数(如calloc )均在 stdlib.h 中,而 newdelete 是语言本身的内存分配机制。

malloc 需要一个参数,void * malloc(size_t _NumOfBytes) ,这个参数表示所需空间的大小,并且不对这段空间进行清空,用法如int *arr = (int *)malloc(n * sizeof(int)); 表示分配 n 个 int 的空间。

其衍生函数,calloc ,需要两个参数,声明形式如void * calloc(int _NumOfElements, size_t SizePerElement) ,用法如int *arr = (int *)calloc(n, sizeof(int)); ,需要注意,这个函数分配后会将这一段内存置为 0。

newdelete 用法

new 用法示例如下:

int *a = new int; // 表示让 a 指向分配的 int 类型值,值未初始化
"上面这一行等价于";
int *a = (int*)malloc(sizeof(int));

int *b = new int(); // 表示让 b 指向分配的 int 类型值,并在分配后采用默认方式初始化这个值
// 对于 int 类型,默认方式初始化意味着置为 0

int *c = new int(3); // 表示让 c 指向分配的 int 类型值,并在分配后初始化这个值为 3
// *c == 3

int *d = new int[n]; // 表示让 d 指向分配的长度为 n 的 int 数组,内存未初始化
"上面这一行等价于";
int *d = (int*)malloc(n*sizeof(int));

int *e = new int[n](); // 表示让 e 指向分配的长度为 n 的 int 数组,并采用默认方式初始化每个值
// e[0] == e[1] == e[2] == ... == e[n-1] == 0

// 特别的,有些类型分配的时候略微麻烦一点(类型系统的进阶使用)
int **a= new (int *[n]); // 长度为 n 的 int* 数组
int (**pf)()= new (int (*)()); // pf 为指向函数指针的指针,指向的函数 f 声明形式如 int f(void);
int (**pfs)()= new (int (*[n])()); // pfs 为函数指针数组

无法用 new 在创建一个数组的同时初始化每个元素


delete 用法类似。(采用new 举例中所用的变量)

delete a;

delete b;

delete c;

delete []d; // 删除数组

delete []e;

new 的思考

能否用new 在创建一个数组后初始化每个元素呢? placement new

函数多态(重载)与默认参数

函数多态

例如,我们有

int max(int a, int b) { return a>b?a:b;}

如果我们想要三个数比较该怎么做?

在 c 中,

int max3i(int a, int b, int c) { int t = max(a, b); return t>c?t:c;}

而在 cpp 中,我们可以直接使用

int max(int a, int b, int c) { int t = max(a, b); return t>c?t:c;}

cpp 可以根据参数的数量和类型来自动判断调用哪一个函数。

以至于我们可以

int max(int a) { int t = a; return t>a?t:a;}
int max(int a, int b) { int t = max(b); return t>a?t:a;}
int max(int a, int b, int c) { int t = max(b, c); return t>a?t:a;}
int max(int a, int b, int c, int d) { int t = max(b, c, d); return t>a?t:a;}
int max(int a, int b, int c, int d, int e) { int t = max(b, c, d, e); return t>a?t:a;}
int max(int a, int b, int c, int d, int e, int f) { int t = max(b, c, d, e, f); return t>a?t:a;}

这时候我们可以传入 1-6 个参数来最大值,并且除了单个参数的情况,其他具有一致性。

再例如,常见的错误代码 int a = pow(5, 2) ,我们可以定义 pow 的多态:

int pow(int a, int n)
{
	if(n<0) return 0;
    int res = 1;
    for(int i=0; i<n; ++i) res *= a;
    return a;
}

可以避免浮点误差。


默认参数

上面介绍了函数多态,这里介绍函数默认参数。

例如我们需要一个求和函数,我们预定有四个数字要被传入。

int sum(int a, int b, int c, int d) { return a+b+c+d;}

但是随着时间流逝,我们有时候需要传入三个参数。

int sum(int a, int b, int c) { return sum(a, b, c, 0);}

以及两个······

int sum(int a, int b) { return sum(a, b, 0);}

甚至一个············

int sum(int a) { return sum(a, 0);}

甚至没有参数(👈这样的函数有意义吗???)

int sum(void) { return sum(0);}

这里的求和函数和上面的 max 形式很像,并且一路规约下来也很自然,只是略微冗杂了点。

能不能更简单呢?

能!

cpp 允许函数含有默认参数。

上面的 sum 系列函数使用默认参数后只需要一个函数即可。

int sum(int a=0, int b=0, int c=0, int d=0) { return a+b+c+d;}

这个函数的定义表示,如果没有传入对应参数,则对应参数默认置为 0。

sum(1,2,3) ,其中各参数即为a=1, b=2, c=3, d=0

在 cpp 中,各参数只能从左向右排布,因此默认参数需要从右向左赋值。

函数的默认参数只能在声明或定义中出现一次。


新的类型

引用

引用类型是值和指针的折中。

引用的主要用途是函数传参和创建别名。

函数传递较大的数据类型由于需要整个拷贝,因此在 c 中通常传递指针作为参数,例如:

typedef struct data
{
    int a[1000000];
    int b[1000000];
}data, *pdate;

int f(pdata p)
{
    pdata->a ...
    pdata->b ...
}

但是频繁的指针操作并不优雅,因此我们使用引用。

int f(data& d)
{
	d.a ...
	d.b ...
}

在这里我们可以像使用一个 data 类型一样使用 data& 类型。

引用和其实体共享一个对象,例如

int a = 2;
int &b = a;
b = 3;

此时 a 也被改变。

class

类是类似结构体的数据类型。

struct A
{
    int a;
} a;

class B
{
    int b;
} b;

int main()
{
    printf("%d\n", a.a);
    printf("%d\n", b.b);
}

类的内容默认对外不开放。

类内可以定义函数(方法)。

class B
{
public:
    int print();
private:
    int b;
} b;

public 表示类外可以访问,private 表示类外无法访问。

这时候我们可以使用 b.print() 来输出,但是不能修改 b.b 的内容。

特别的,cpp 中的 struct 也不同于 c 中的,它更像是被修改了默认访问权限的 class。


内联与宏

内联的好处

宏是一种功能强大的预处理工具。

但是含有隐患。

##define mult(a, b) a*b

如果使用

int x = mult(3+2,2+3);

如常理所想应该是 x 被赋值为 (3+2)*(2+3) == 25,但是实际上,这句在展开后会成为

int x = 3+2*2+3; // x == 10

诚然,我们可以采用加括号的方式来封闭宏,但是总有力所不逮之处(为什么不行?思考题)。

因此,cpp 创建了内联的语法。

inline int mult(int a, int b) { return a*b;}
int x = mult(3+2,2+3);

这时候,第二行int x = mult(3+2,2+3); 就相当于展开成这样的三行:

int a = 3+2;
int b = 2+3;
int x = a*b;

可以确保其正确性。

为什么还要宏?

内联如此方便、强大、安全,为什么要宏呢?

这里是宏的一些(内联做不到的)用法:

##include <cstdio>

int test_function(void);

int main()
{
    test_function();
	printf("%s\n", __FUNCTION__);
}

int test_function(void)
{
	printf("%s\n", __FUNCTION__);
}

例如上面的 mult ,也可以使用宏的方式来正确处理。

##define mult(a, b) ((a)*(b))

但是,它在维持原有参数结构的情况下,不能做到和内联一样安全。

自定义主题的时候遇到的一些问题记录如下:

看板娘配置

参考网上教程配置了 Live2D 看板娘。

维护 Continue
维护 Continue