最近准备重新学习学习C++,因为之前也都是上课学习,其实很多细节自己还是没有掌握地很好,现在时间比较充裕,乘着这个窗口期赶紧再充实一下自己。这系列的文章既是每周的总结也是希望跟读者们共同进步,相互学习。
这篇文章并不是讲C++的语法,而是C++学习的预热。内容看起来很杂,其实是一些自己感觉很熟的东西中容易忽略的地方。主要是建立学习C++的好奇与兴趣。正如某位大佬所说:“技术领域并不缺少神奇,只是缺少发现神奇的眼睛。”
主要内容包括:c++工程开发规范;编译链接的具体过程;如何在c++工程中引入第三方模块以及预处理。
说明:部分内容源自自己购买的付费课程,若有侵权,请联系删除。
<面向对象>
对于最开始的一段代码:
1 |
|
其中,cout到底是什么东西呢?
->实际上,cout在c++中是一个预定义的对象。
<运算符重载>
目的:通过运算符重载可以输出自定义的对象,让不同类型的对象使用更加方便,符合编码逻辑
1 | #include<iostream> |
其中,思考一下两个问题:
operator<< 返回的是左值引用。但什么是左值引用呢?
可以实现连续赋值的操作,如==,+=,<<等。这里就是说,cout<<a之后仍返回一个ostream的实例。
更简单的解释是:下面这个例子
1
2
3
4int a=5,b=7;
(a=b)=10;
//result: a=10, b=7具体过程:b先赋值给a,返回一个int对象的引用(a的引用),然后再将10赋值给这个引用,所以最终才会有这个答案。若(a=b)返回的不是左值引用,而仅仅是一个值,那么int值=10就编译不通过了(报错为:error: left value required as left operand of assignment)。
小结:连续运算符的重载应返回左值引用
为什么第二个参数要为const类型?
- 不希望对类成员变量进行修改。
- 加上const,对于const和非const的实参,函数都能接受。但不加的话,就只能接受非const的实参。编译不能通过(报错为:error:
no match for ‘operator<<’ (operand types are ‘std::ostream {aka
std::basic_ostream
}’ and ‘const Point’))
同时补充一下,使用引用&Point 是因为 可以避免在函数调用时对实参的一次拷贝,提高了效率。
<工程开发规范>
对于<运算符重载>部分的代码:
存在以下问题:
- 没有将类的声明与定义分离
- 暴露了类的成员变量
- 命名空间的使用,不清楚到底引入了多少符号!
修改如下:
1 |
|
但是注意,类的成员变量若是私有,外部需要访问就需要将 使用了这个私有成员变量的函数 声明为友元函数。
<c++为什么难学>
有以下几个原因:
c++语法难学,多样复杂
“技术想象力”要求高(用“编程范式”来举例了,如:面向过程,面向对象,泛型编程,函数式编程。即设计代码的思想),来源于夯实的基础知识。为什么会看不懂开源代码?就是由于基础知识的匮乏导致的技术想象力有限。
技术想象力的例子,以c++中实现加法为例:
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
27
28
29
30
31// 面向过程
int add1(int a,int b){
return a+b;
}
// 面向对象
class ADD{
public:
int operator()(int a,int b){
return a+b;
}
};
// 泛型编程
template<typename T,typename U>
auto add3(T a, U b) -> decltype(a+b){
return a+b;
}
// 函数式编程
auto add4 = [](int a,int b) -> int {
return a+b;
};
int main(){
ADD add2;
cout<<add1(3,4)<<endl;
cout<<add2(3,4)<<endl;
cout<<add3(3,4)<<endl;
cout<<add4(3,4)<<endl;
return 0;
}
// main中,看似使用的一样,但是背后实现完全不同!其实,为什么c++语法特性多,难学,是因为c++要同时支持4种编程范式。
c++背后承载的体系特别大。不仅仅是c++语言本身,除此之外,c++跟底层系统的联系,和网络的联系,所透露出算法数据结构的思想,不同的编程范式等。一个真正掌握c++开发的程序员,背后的这套体系极其夯实庞大!
C程序执行的过程
C源码变成可执行文件的重点的两个过程(一共有6个过程):
对象文件中,存放的是“定义”
多个C源码被编译为多个对象文件,然后将多个对象文件链接成一个可执行文件。
<声明与定义>
- 声明:相当于一个说明的作用
- 定义:表示具体的实现过程
1 | // 声明 |
对于对象文件是什么感到好奇吗?
<编译阶段>
编译阶段的主要目的:语法检查。即确保c源码没有语法错误,能生成对象文件。
只执行编译的命令:
1 | g++ -c main.cpp |
查看对象文件main.o中到底是什么东西:
1 | nm -C main.o |
得到以下的结果:
其中,前面有一串数字以及T字母的 这些方法是当前文件中定义的;没有数字的 表示那些方法的定义是在外部进行查找的。
思考:那些需要外部查找定义的函数是如何被找到的呢?
注意:编译阶段不需要知道所声明函数的定义
<链接阶段>
链接阶段的主要目的:将多个对象文件拼接在一起。把对象文件中需要外部查找的方法与其具体的定义链接起来。
相当于,你有病(需要外部查找的方法),我有药(方法的定义),我们之间匹配上了。
举例:
(1)main.cpp --> main.o
1 |
|
(2)add.cpp --> add.o
1 | int add(int a,int b){ |
接着将这两个对象文件连接在一起,就可以形成一个完整的可执行程序。
1 | g++ main.o add.o |
然后执行这个可执行文件
1 | ./a.out |
<小结>
- 编译阶段主要在做 语法检查, 而链接阶段主要在做 定义匹配。
- 声明作用于编译阶段,帮助源程序通过编译(编译期间出现的错误主要有:符号未定义/找不到,语法错误等);定义作用在链接阶段,帮助对象文件完成定义的匹配(链接阶段出现的错误主要有:定义不存在或者定义冲突)
- 声明主要放在头文件中,定义主要放在源文件中
google测试框架
重点:学习如何在c/c++工程中引入第三方功能模块
我们在编译阶段只需要第三方模块的声明(在头文件.h中),在链接阶段,我们需要第三方模块的定义(静态链接库.a文件)
googletest提供的是cmake方法进行编译,那么新建build文件夹并对googletest模块进行编译,得到如下结果:
1 | zzmine@ubuntu:~/myLibs/googletest/build$cmake ../ |
注意这里生成了4个.a文件,并且有一个lib文件夹,这个就是第三方模块googletest中方法的定义!
然后在xxx/googletest/googletest/ 中可以找到include文件,这就是第三方模块googletest中方法的声明!
接着就可以使用googletest编写C++程序了:
1 |
|
但是,在执行编译过程时,发生以下报错:
1 | zzmine@ubuntu:~/CPractice$ g++ -c main.cpp |
说明,其实在默认的头文件搜索路径中,没有包括googletest的include文件夹。
所以需要告诉g++编译器,在编译搜索目录时需要加上googletest的include路径。执行(这里我把googletest的include路径拷贝到当前目录中了):
1 | g++ -I./include -c main.cpp |
完成了编译过程,此时在当前目录中生成了一个main.o文件。用nm -C main.o查看对象文件内容,发现有大量的外部符号链接。
接下来进行链接过程,将main.o与之前生成的googletest静态链接库文件进行链接。
1 | g++ -L./lib main.o -lgtest |
最后,执行可执行文件 ./a.out 获得想要的输出结果。
预处理
C++源文件通过预处理得到待编译源码,待编译源码决定了可执行文件的功能。
所有预处理命令都是以#开头,其中宏定义是预处理家族中的一员,宏做的事情就是简单的替换。
<宏定义>
定义符号常量
1
2
3定义表达式
1
2定义代码段:注意,每行后面要加 表示连接,因为预处理命令只能写在一行
1
2
3
printf("%d\n",a); \
}
备注:#include 表示将后面库文件的内容添加到当前文件中。
举例:define.cpp
1 |
|
可以执行一下命令来查看待编译源码的内容:
1 | g++ -E define.cpp |
也可以通过重定向文件输出到某个文件中进行查看:
1 | g++ -E define.cpp > output.cpp |
发现主函数部分被替换成了如下的代码:
1 | // ... 上面省略了很多产生的内容 |
需要特别注意的是,可执行文件的功能完全取决于待编译源码!
可以举出下面的一些例子:
1 | #define S(a,b) a*b |
<预定义的宏>
宏 | 说明 |
---|---|
__ DATE __ | 日期:Mmm dd yyyy |
__ TIME __ | 时间:hh:mm:ss |
__ LINE __ | 行号 |
__ FILE __ | 文件名 |
__ FUNC __ | 函数名/非标准 |
__ PRETTY_FUNCTION __ | 更详细的函数信息 |
反思
通过这种探索式的学习,自己发现其实C++中有很多有趣的内容,而这些在之前的课程学习中完全被忽略掉了,当时的我只是很功利地去模拟书上的代码,却很少静下心来领略编程的艺术。
同时,我在重学C++的过程中,全程使用的是Linux平台的g++编译器以及vim编辑器,而没有选择更加“方便”的IDE。在学习的过程中,我对c++文件从文本到可执行文件的过程有了更深的理解。我明显地感觉到自己对C++的理解更加深刻了,其实这种感觉也说不上来是什么样子,大概就是代码打起来手比较熟,报错啥的完全不慌哈哈哈哈哈。印象最深刻的就是可执行文件的功能其实完全取决于预编译源码,而并不是自己写的源代码。感觉正确的编程学习方式还是要基于原理,然后大规模开发需要提升效率再使用IDE。
最后,这并不是一周所学的全部内容,只是这些都是C++预热的部分,而且全放在一篇文章会显得很臃肿,所以其余内容将会在后续发布。
若有任何疑问,欢迎交流!