委员长 发表于 2024-12-28 23:03:45

C++Primer学习笔记01.Welcome to C++

一些基础内容,只记录一些必要的。

cmake 文件

```cpp
cmake_minimum_required(VERSION 3.10.2)
project(项目名)

set(CMAKE_CXX_STANDARD 11)

# 遍历项目根目录下所有的 .cpp 文件
file (GLOB_RECURSE files *.cpp)
foreach (file ${files})
    string(REGEX REPLACE ".+/(.+)\\..*" "\\1" exe ${file})
    add_executable (${exe} ${file})
    message (\ \ \ \ --\ src/${exe}.cpp\ will\ be\ compiled\ to\ bin/${exe})
endforeach ()
```

### ASCII 表

`a-->97`,`a-->65`,`0-->48`,`空字符-->32`

### 简单的C++程序

返回值 -1 通常被当作程序错误的标识。重新编译并运行你的程序,观察你的系统如何处理main返回的错误标识。将 main 函数的返回值修改为 -1,观察结果。

```cpp
#include<iostream>
using namespace std;

int main(){
    cout<<"modify return value"<<endl;
    return -1;
}
// 控制台提示
// Command executed failed(Exit code 255)
```

### 输入输出

C++ 的 iostream 库包含两个基础类型 istream 和 ostream,分别表示输入流和输出流。

#### 标准IO对象

标准库定义了 4 个 IO 对象。为了处理输入

- 名为 cin(发音为 see-in)的 istream 类型的对象。这个对象被称为标准输入(standard input)。
- 名为 cout(发音为 see-out)的 ostream 类型的对象。此对象被称为标准输出(standard output)。
- 名为 cerr 和 clog(发音分别为 see-err 和 see-log)的 ostream 类型对象。我们通常用 cerr 来输出警告和错误消息,因此它也被称为标准错误(standard error)。而 clog 用来输出程序运行时的一般性信息。系统通常将程序所运行的窗口与这些对象关联起来。

当我们读取 cin,数据将从程序正在运行的窗口读入,当我们向 cout、cerr 和 clog 写入数据时,将会写到同一个窗口。

```cpp
#include<iostream>
using namespace std;

int main(){
    int n1,n2;
    // cout 使用 << 将字符串给定到 cout 对象中
    // endl 将缓冲区的数据刷到设备中。
    std::cout<<"please input two number"<<std::endl;
    // 使用 >> 将控制台的输入给定到 n1,n2 中
    std::cin>>n1>>n2;
    std::cout<<"result is:"<<n1+n2<<std::endl;
    return 0;
}
```

程序使用了 `std::cout` 和 `std::endl`,而不是直接的 cout 和 endl 。前缀 `std::` 指出名字 cout 和 endl 是定义在名为 std 的命名空间(namespace)中的。命名空间可以帮助我们避免不经意的名字定义冲突,以及使用库中相同名字导致的冲突。标准库定义的所有名字都在命名空间 std 中。

习题:解释下面程序是否合法,合法输出什么,不合法原因是什么,如何修改

```cpp
#include<iostream>

// 解释下面程序片段是否合法
int main(){
    int v1,v2;
    std::cout<<"The sum of "<<v1;
    << "and "<< v2;
    << " is "<< v1+v2 <<std::endl;
    return 0;
}
/*
不合法, ; 表示这跳语句终止了,第二条语句的 << 就不知道给定到(流的重定向?)那个对象了。
去掉 v1,v2 后面的 ; 即可
*/
```

使用 setprecision 设置输出的精度,在头文件 `iomanip` 中。刷题要求输出限制精度的时候常用,当然直接用 C 的 `%.4f` 这种限制位数的更直接。

```cpp
#include <stdio.h>
#include<iostream>
#include<iomanip>

using namespace std;

int main() {
    double dv = 10.2356452;
    printf("%.4f", dv); // 10.2356
    cout << setprecision(5) << dv << endl; // 10.236
    // fixed 取小数点后xx位                                             
    cout << fixed << setprecision(2) << dv << endl; // 10.23
}
```

> C 语言输入输出的控制符

`int: %d`
`float: %f`
`double: %lf`
`char %c`
`long long %lld`

### 注释

两种注释,单行注释和多行注释

```cpp
//
/*
*
*/
```

```cpp
#include<iostream>

// 解释下面程序片段是否合法
int main(){
    int v1,v2;
    std::cout<<"/*"<<std::endl;
    std::cout<<"*/"<<std::endl;

    // 正确
    std::cout<</*"*/" /*"/*"*/<<std::endl;

    return 0;
}
```

### 控制流

if、else、for、while;

读取数量不定的输入数据

```cpp
#include<iostream>

int main(){
    int value=0,sum=0;
    while(std::cin>>value){
      sum+=value;
    }
    std::cout<<sum<<std::endl;
    return 0;
}
/*
Linux 下最后输入 EOF 就可以停止了。
*/
```

当从键盘向程序输入数据时,对于如何指出文件结束,不同操作系统有不同的约定。在 Windows 系统中,输入文件结束符的方法是敲 Ctrl+Z(按住 Ctrl 键的同时按 Z 键),然后按 Enter 或 Return 键。在 UNIX 系统中,包括 Mac OS X 系统中,文件结束符输入是用 Ctrl+D。

### 类介绍

不记

### C++如何工作的

C++ 文件编译的时候,包含进来的文件(头文件)一起被编译了。每个 C++ 文件都被编译成了一个 object file (目标文件),这些 object file 会被合并成一个可执行文件。
link 将所有的 obj 文件合并成一个可执行文件。

编译-->链接-->运行。

### 编译是如何工作的

C++ 代码的运行包含这几步:编译-->链接-->运行。而 #include 在编译中的含义则是`复制粘贴`。来看下下面的例子。

#### #include

```cpp
// EndBrace.h 头文件, 仅包含一个 }
}
```

```cpp
// Math.cpp 文件, 缺少了一个 }, 但是用 #include"EndBrace.h" 引入了头文件中的内容, 正好补上了缺少的括号。
int Multiply(int a, int b) {
        int result = a * b;
        return result;

#include"EndBrace.h"
```

将 VS 中『项目属性=\=>C/C++=\=\>预处理器=\=\>预处理到文件=\=>是(/P)』 可以看到预编译后的文件结果如下。

```cpp
#line 1 "C:\\development\\Code\\CPlusPlus\\Video\\Math.cpp"
int Multiply(int a, int b) {
        int result = a * b;
        return result;

#line 1 "C:\\development\\Code\\CPlusPlus\\Video\\EndBrace.h"
}
#line 6 "C:\\development\\Code\\CPlusPlus\\Video\\Math.cpp"
```

从上面的例子可以看出,#include 在编译时候的作用就是告诉编译器,把 #include 的内容复制粘贴过来。

#### 预处理语句

有些代码只在 windows xp 平台有效,有些代码只适用于 windows 10,这种情况我们可以使用预处理语句来进行选择,根据条件来激活相应的代码。

```cpp
#include<iostream>
#if 1        // 当这里的判断为真时,被其包裹的代码会被复制粘贴到预处理后的文件中。为 false 则不会包含。
int say() {
        std::cout << "this is one" << std::endl;
}
#endif
```

#### 链接后的文件内容

如果我们直接打开 C++ 链接后的二进制文件,会看到一系列的数字。此时我们可以修改项目的属性『项目属性=\=>C/C++=\=\>输出文件=\=\>汇编输出=\=>仅有程序集的列表 (/FA)』。这样就可以看到对应的汇编代码。

现在我们将 Multiply 对应的文件输出为汇编代码。

```asm
# 可以看到有两次 mov 操作, 因为我们是把结果赋值给了 result 而不是直接返回
mov        eax, DWORD PTR a$
imul        eax, DWORD PTR b$
mov        DWORD PTR result$, eax

# 直接返回的汇编结果, 只有两天汇编指令。
mov        eax, DWORD PTR a$
imul        eax, DWORD PTR b$
```

上述对比结果告诉我们,需要开启代码优化,提高代码的运行速度(调试模型下默认是不开启优化的,但是 release 模式默认是开启的,依旧可以通过`项目属性`进行修改)。

接下来,我们尝试用函数调用函数,看看对应的汇编代码是怎么样的。

```cpp
const char* Log(const char* message) {
        return message;
}

int Multiply(int a, int b) {
        //int result = a * b;
        //return result;
        Log("Print Log");
        return a * b;
}
```

对应的汇编代码如下

```asm
; Line 8
        lea        rcx, OFFSET FLAT:??_C@_09IGJOIAJB@Print?5Log@
        call        ?Log@@YAPEBDPEBD@Z                        ; Log        // 这个就是函数签名, 唯一的定义了函数。
; Line 9
        mov        eax, DWORD PTR a$
        imul        eax, DWORD PTR b$
```

链接器就是为了把所有 obj 文件链接在一起,查找函数签名。调用函数时按函数签名来定位对应的函数体。

### 链接是如何工作的

#### 作用

现在,我们来编写两个 C++ 文件,一个 Log.cpp 包含一个打印日志的函数,一个 main.cpp 调用了 Log#log 函数。两个 cpp 文件中的代码如下所示。

```cpp
// Log 函数用到了输出方法,因此映入了头文件 iostream
#include<iostream>

void Log(const char* message){
    std::cout<<message<<std::endl;
}
```

```cpp
#include <iostream>
// main 中调用了 Log 函数,但是 main 不知道 Log 函数是什么?因此我们需要声明一个 Log 函数。那么在调用 Log 的时候,编译器又是如何找到 Log 函数的函数体的呢?这一切就归功于 Link 了。
void Log(const char* message); // 告诉编译器,这里有个 Log 函数,你相信我就行。而实际运行的代码(函数体)Link 会帮我们找到。
int main() {
    Log("Print Log");
    std::cout << "Hello, World!" << std::endl;
    return 0;
}
```

我们构建整个工程的时候,所有文件都会编译,Link 会找到正确的 Log 函数的定义在哪里,将函数定义导入到 main.cpp 中的 Log 中,让我们在 main.cpp 中调用。如果找不到会出现 link 错误。

我们将 Log.cpp 中的函数名改为 Logs,这样 main.cpp 在链接的时候就找不到函数了,会出现链接错误。

```shell
CMakeFiles/C++.dir/main.cpp.obj:C:/Code/C++/main.cpp:6: undefined reference to `Log(char const*)'

xxx无法解析的外部符号(Link无法解析外部符号),Link 的工作是链接函数,但是它找不到对应的函数,报错了。
```

链接主要聚焦的是找到每个符号和函数在哪里并把他们链接起来。每个文件(\*.cpp \*.c)会被编译成一个单独的目标文件,它们彼此之间没有联系,文件之间不能交互;而链接器可以建立文件之间的联系,这样及时外部文件中没有这个函数,只要他知道函数在哪里,就可以使用。

#### 错误

<b>错误类型</b>

程序运行的时候会出现两种错误,一种是编译错误,一种是链接错误。在 VS 中,编译错误以 C 开头,链接错误以 LNK 开头。

```shell
error C2143: 语法错误: 缺少“;”(在“std::cin”的前面)
```

如果我们试图运行一个没有 main 函数(去除项目的 main 函数)的项目会出现链接错误。

```shell
fatal error LNK1120: 1 个无法解析的外部命令
```

因为项目默认的执行入口是 main,当然我们也可以指定其他的入口:『项目属性=\=\>链接器=\=\>高级=\=\>入口点』

<b>常见错误一</b>

假定有两个文件 Log.cpp 和 Math.cpp,Math.cpp 中只声明了 Log 函数但是没有函数体。

```cpp
// Log.cpp
#include<iostream>
void Logger(const char* message) {
        std::cout << message << std::endl;
}

// Math.cpp
const char* Log(const char* message) {
        return message;
}

int Multiply(int a, int b) {
        //int result = a * b;
        //return result;
        Log("Print Log");
        return a * b;
}

int main() {
        std::cout << "hello" << std::endl;
}
```

编译上述代码不会出现错误,链接时会出现错误。虽然我们并没有使用 Multiply 但是它可能会在其他地方被使用,由于找不到这个函数签名的函数体,所以链接的时候报错了。

一个解决办法是将函数声明为 static,这意味着该函数只在这个翻译单元内有效(这个文件内),也就意味着不可能被其他文件使用,可以正常链接、运行。

```cpp
#include<iostream>

// 正常运行
const char* Log(const char* message) {
        return message;
}

static int Multiply(int a, int b) {
        //int result = a * b;
        //return result;
        Log("Print Log");
        return a * b;
}

int main() {
        std::cout << "hello" << std::endl;
}
```

<b>常见错误二</b>

如果相同签名的函数体重复定义,在编译的时候不会出错误,但是在链接的时候编译器会不知道到底该找那个函数体然后报错。虽然这种情况看起来不太可能发生,我们在编写代码的时候会避免发生这种情况,但是由于 #include 复制文件的操作,这种重复的定义函数体是非常有可能的。

```cpp
// Log.cpp 定义函数
#include<iostream>

void Logger(const char* message) {
        std::cout << message << std::endl;
}
```

```cpp
// Math.cpp 引入
#include<iostream>
#include"Log.cpp"

int main() {
        std::cout << "hello" << std::endl;
}
```

```cpp
// Video.cpp 引入
#include<iostream>
#include"Log.cpp"
```

编译结果一切正常,链接报错。

```shell
1>Math.obj : error LNK2005: "void __cdecl Logger(char const *)" (?Logger@@YAXPBD@Z) 已经在 Log.obj 中定义
1>Video.obj : error LNK2005: "void __cdecl Logger(char const *)" (?Logger@@YAXPBD@Z) 已经在 Log.obj 中定义
1>C:\development\Code\CPlusPlus\Debug\Video.exe : fatal error LNK1169: 找到一个或多个多重定义的符号
```

因为 #include 本质就是把代码复制粘贴过去,两次 #include 函数体,相对于多重定义的。

解决办法有三种

- 将方法声明为 static,这意味着函数被链接的时候只能是内部函数(只在文件内有效,也就不会存在重复定义的问题了)
- 将方法声明为 inline,这意味着代码会直接被复制到调用它的函数内部,也不存在函数调用开销了。
- 将函数声明和方法体的定义分开来 .h 文件声明方法,.cpp 文件定义方法。引入的时候只引入 .h 文件。
页: [1]
查看完整版本: C++Primer学习笔记01.Welcome to C++