为什么学习C++
C++应该是目前主流语言中最难的一种了,相比于之前学习过的Java和python,C++既有C语言的严谨与细致、对内存的绝对把控,也包含着面向对象的思想,在各个领域也均有建树。之所以学习它,因为现在无论是Java也好还是python也好,丰富的第三方依赖已经让我不会实现具体的功能,习惯于拿来即用的我发现在实现某些功能的时候已经忘记了如何去写。既然决定学习下去,那么与君共勉。
学习的准备
-
学习视频 :目前在b站上学习C++,用的C++大佬cherno的教学视频,可惜翻译目前尚未完成,后面的视频学习起来还是有点困难的,如果有愿意一起翻译视频的可以私聊我。
-
IDE:IDE当然选择的是宇宙第一的visual studio!
C++基本语法
基本语法这一块我不会去记录下来,毕竟各种语言的基本语法大同小异,菜鸟教程的内容很良心了,我在学习一门语言之前都会去菜鸟教程上面先学习一下基本语法,也就是定义变量,运算符,if,循环,函数定义这些内容,主流的语言在这些方面都是差不多的。
C++的compiler与link
学习C++的第一天就在学习编译与链接,之前在学习Java,python和入门的C语言时都没有细心地了解过compiler与link,之前在学习opencv的时候曾经使用过C++,然而还是很快的转战到了python,当时在配置opencv的环境时,曾经很苦恼于各种路径的配置,当时只是找了篇文章照猫画虎,现在想想,当时完全没有理解link。不了解link带来的结果就是在写C++的时候经常遇到Link error,对于python友好的pip,和java的万能的maven框架,C++的link更让我理解的语言的底层操作。也许正是因为C++学习起来总会涉及到计算机原理的一些东西,让我对学习它产生了浓厚的兴趣。
1. Compiler(编译)
C++的编译主要可以大致分为预编译和编译两个部分,接下来分别学习一下什么是预编译,什么是编译。在学习此处的过程中将极大地加深我们对C++工作模式的理解,对我们以后项目编写时要注意些什么会有很大的帮助。
- 预编译
在我们写C++语言的时候最先写的可能都会是#include<iostream>
,这行代码到底充当了一种什么角色?我们发现当我们导入一些外部的头文件时,我们都会使用#include
。这个时候我们会了解到,这个关键词的作用是用来导入别的文件的代码到这个文件来,让我们可以使用外部函数。学习到后面我们又会学习到#define
,这个代码帮助我们定义一些不可修改的变量,我们也知道它的名字宏定义,现在我们就接触到了预编译的关键词宏。
首先我们要知道预编译到底是在做什么,它是发生在正式编译之前的操作,核心的任务就是选择哪些代码参与到编译,哪些代码不参与到编译。
所以首先我们可以大致了解以下的预编译指令:
这里我们重点看一看1、包含指令:#include 2、条件指令:#if、#elif、#else、#endif、#ifdef、#ifndef 3、定义指令:#define、#undef
#include
和#define
。
#include
: 这个命令是指定一种包含关系,告诉编译器当前编译单元还包含了哪些外部文件,有哪些外部代码也是要参与到编译之中的而不是仅仅编译当前的文件。这里大家会发现有时候我们使用<>
指定外部文件,有时候我们却又使用""
指定外部文件,二者的区别主要是前者将以编译器默认的系统头文件目录下去寻找指定文件,而后者是从当前源文件的目录下寻找指定文件。
#define
: 这个指令是只要就是指定一种替换关系,当我们使用#define PIE 3.14
,我们就是指定PIE
这个变量的值为3.14,这种替换因为发生在正是编译之前,所以我们不会提前分配内存,则宏定义不会有变量类型的检查也不会进行计算,所以不仅可以指定一种数字或者字符,还可以指定一些简单的函数,例如#define Square(x) x*x
。这是严格的直接替换,后续无法被修改,只有使用#undef
指令取消宏定义。
其他的指令更多用在头文件引用的保护上,这里会涉及很多头文件和链接的知识,我们到头文件的部分再来仔细探究它的作用。 - 编译
C++的编译主要分为九个阶段:字符映射、行合并、标记化、预处理、字符集映射、字符串连接、翻译、处理模板、链接。这不是我们今天要学习的主要内容,了解这些只是知道编译器工作的流程,我们更加要注重在编译器对我们的C++项目做了什么,怎么实现的编译?
首先我们应该知道编译单元,编译单元是C++编译的基础,即编译器对每一个.cpp
文件是当作一个独立的编译单元进行编译的,一个单元编译的范围也仅仅只在这个.cpp
文件当中。但是我们在写C++程序的时候不可能所有的东西都写在一个文件里,我们会在预编译的时候告诉编译器哪些文件之间是有关联的,这个就叫链接。
分别编译就是C++编译的另一个重要的理论,编译器会通过链接知道哪些文件之间是需要一起编译的,我们的#include
就是用来做这个的,我们告诉编译器我们需要从哪个文件里调用它的代码,编译器编译的时候会把include
里面的文件代码一起复制到当前的编译单元里,实现了虽然在是单独的单元编译,也可以分别编译两个源文件的代码。
C++通过编译生成了obj文件,这里包含了对应的机器语言,这是给计算机看的,里面包含的代码对应了变量和内存之间的联系,寄存器的操作,这就涉及到组成原理的知识。我们可以通过visual studio在调试的时候进入机器语言和内存,看到每一步的操作与变化。相比更加高级的python和java,C++这种直击底层的特性十分具有魅力。
2. Link(链接)
在了解链接之前我们首先要明白什么是头文件,也就是.h
文件,写过C++代码的同学一定发现,我们在看到别人写好的的项目里会大量出现两种文件,一个是源文件.cpp
,另一个就是头文件.h
。当你打开头文件,你会发现里面没有什么具体的逻辑代码,只有数不清的函数或者类的定义,别着急这个时候你就要细心观察一下我们之前写代码经常include
的一个文件stdio.h
,这个头文件应该绝大多数学过C语言的同学都用过,现在我们了解了编译和#include
指令后,我们发现这个文件是用来链接的,也可以说我们需要里面的代码,可是头文件里面只有函数定义并不存在函数的逻辑代码,导入它有什么用呢?这个时候我们就要开始了解C++是如何链接的了!
- 头文件的作用
假设我们这里有main.cpp
它的代码如下:
明显这个地方调用了一个外部函数// main.cpp int main() { Log("Hello, World!"); retuen 0; }
Log()
,假设这个函数在Log.cpp
中,但是我们不能直接去include
这个文件,你也会发现C++也不支持这样做。因为我们仅仅只需要该文件里面的一个函数,而不需要其他代码,直接导入可能会在编译这个单元的时候编译许多无关的代码。这个时候我们想到了头文件,头文件里面只存在函数和类的定义,与宏定义。到了正式编译的时候,我们并不会去编译这个头文件,因为我们导入了大量的函数定义到了main.cpp
中,编译器是不会在乎这些函数有没有实例化,因为定义函数不实现是没有错误的,函数的定义在编译中只是申请了一块内存我们需要使用这个函数。直到编译到Log("Hello, World!");
这一句的时候,编译器需要找到这个函数具体的实现代码了,这个时候链接的作用就出来了。到这里头文件的作用大概清楚了,它的存在让我们对于不同文件之间的关系有了一个缓冲的地区,我们把函数的声明都写在这里,这样谁需要用的时候,只需要导入一个头文件就好了,并且里面并没有实际的代码,也不会存在代码重复编译的情况。 - 链接
到这里我们就要进一步探究链接了,链接也可以分为两种,一种是调用cpp代码,一种是调用链接库。
那么我们首先学习cpp代码的链接,这里很简单,使用头文件去链接不同的cpp文件,上述案例中,Log函数的定义在Log.cpp中,main.cpp调用了Log函数,我们使用utils.h头文件链接二者:
实例中,// utils.h void Log(string message); // main.cpp #include "utils.h" int main() { Log("Hello, World!"); retuen 0; } // Log.cpp #include "utils.h" void Log(string message) { std::cout << message << std::endl; retuen 0; }
utils.h
只负责函数的声明。那么在正式编译的时候,这条声明的命令就会加载到main.cpp
里,编译器发现了函数声明,于是通过头文件的引用发现了Log.cpp
同样引用了该头文件,并且里面有Log函数的的具体实现,于是一并加载到main.cpp
文件中,实现了不同cpp文件的链接。
接下来是链接库的链接, 在一个项目中肯定会使用到不同功能的模块,以opencv为例,当使用opencv的时候,官网提供给你的往往只有头文件和.dll
和.lib
文件,而不是大量的cpp代码,因为太大的项目,许多功能不需要重复编译,像opencv这种第三方算法库,提供编译好的链接库,在后续的调用运行会大大提升编译速度和开发效率。所以.dll
文件为动态链接库,.lib
为静态链接库,你可以把它们当作编译好的cpp文件,所以它们的使用也必须对应着头文件来用,比如#include<stdio.h>
,我们找不到它对应的具体cpp文件,因为我们只需要它的函数声明,具体的代码早就编译成链接库。这里的理解可以当作java里面的jar包和python里面的第三方库,一定要注意,C++里面的链接都是通过头文件将不同的cpp链接起来,所以虽然dll和lib是编译好的文件,但是依旧需要对应的头文件去使用它们。
Debug和Release
我们在使用visual studio去运行我们的代码的时候,会发现上面有debug和release两种模式,还有x64和x86的选项,后者比较好理解,你的代码是在64位的计算机上运行就要选择x64,是在32位的计算机上运行就要选择x86。而前者可能感觉不出来差异,其实debug的模式是开发时常用的,这种模式下代码的编译不会进行优化,包含许多调试信息,可以让我们对其进行一步步的调试,甚至深入到内存里的数据变化;而release版本是发布版本,一般都是调式无错后使用,这种情况下代码编译速度大大提升,编译器进行许多优化,但是不包含调试信息,虽然可以进行调试,但是底层的内存错误就不能定位到具体位置了。
结语
C++学习的道阻且长,了解编译和链接预备的知识,我们以后在遇到报错的时候可以更好的定位自己的问题,也让我们对C++这门语言的特性有一定的了解。其中,C++的链接确实是很多C++新手头疼的地方,配置环境的大敌,我们一起学习,一起克服!