登录  | 立即注册

游客您好!登录后享受更多精彩

查看: 141|回复: 0

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

[复制链接]

44

主题

-24

回帖

30

积分

新手上路

积分
30
发表于 2024-12-28 23:03:45 | 显示全部楼层 |阅读模式

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

cmake 文件

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-->97a-->650-->48空字符-->32

简单的C++程序

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

#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 写入数据时,将会写到同一个窗口。

#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::coutstd::endl,而不是直接的 cout 和 endl 。前缀 std:: 指出名字 cout 和 endl 是定义在名为 std 的命名空间(namespace)中的。命名空间可以帮助我们避免不经意的名字定义冲突,以及使用库中相同名字导致的冲突。标准库定义的所有名字都在命名空间 std 中。

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

#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 这种限制位数的更直接。

#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

注释

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

//
/*
*
*/
#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;

读取数量不定的输入数据

#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

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

#include"EndBrace.h"

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

#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,这种情况我们可以使用预处理语句来进行选择,根据条件来激活相应的代码。

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

链接后的文件内容

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

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

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

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

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

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

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;
}

对应的汇编代码如下

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

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

链接是如何工作的

作用

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

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

void Log(const char* message){
    std::cout<<message<<std::endl;
}
#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 在链接的时候就找不到函数了,会出现链接错误。

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 开头。

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

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

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

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

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

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

// 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,这意味着该函数只在这个翻译单元内有效(这个文件内),也就意味着不可能被其他文件使用,可以正常链接、运行。

#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 复制文件的操作,这种重复的定义函数体是非常有可能的。

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

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

int main() {
    std::cout << "hello" << std::endl;
}
// Video.cpp 引入
#include<iostream>
#include"Log.cpp"

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

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 文件。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|断点社区 |网站地图

GMT+8, 2025-1-19 02:35 , Processed in 0.066918 second(s), 27 queries .

Powered by XiunoBBS

Copyright © 2001-2025, 断点社区.

快速回复 返回顶部 返回列表