C++基础知识回顾——预热

最近准备重新学习学习C++,因为之前也都是上课学习,其实很多细节自己还是没有掌握地很好,现在时间比较充裕,乘着这个窗口期赶紧再充实一下自己。这系列的文章既是每周的总结也是希望跟读者们共同进步,相互学习。

这篇文章并不是讲C++的语法,而是C++学习的预热。内容看起来很杂,其实是一些自己感觉很熟的东西中容易忽略的地方。主要是建立学习C++的好奇与兴趣。正如某位大佬所说:“技术领域并不缺少神奇,只是缺少发现神奇的眼睛。”

主要内容包括:c++工程开发规范;编译链接的具体过程;如何在c++工程中引入第三方模块以及预处理。

说明:部分内容源自自己购买的付费课程,若有侵权,请联系删除。

<面向对象>

对于最开始的一段代码:

#include<iostream>
using namespace std;

int main(){
    cout<<"hello world"<<endl;
    return 0;
}

其中,cout到底是什么东西呢?

->实际上,cout在c++中是一个预定义的对象

<运算符重载>

目的:通过运算符重载可以输出自定义的对象,让不同类型的对象使用更加方便,符合编码逻辑

#include<iostream>
using namespace std;

class Point{
public:
     Point(int x,int y):x(x),y(y){}
     int x,y;
};

//重载运算符
ostream &operator<<(ostream &out, const Point &p){
    out<<"("<<p.x<<","<<p.y<<")";
    return out;
}

int main(){
    Point a(3,4);
    cout<<"hello world"<<endl;
    cout<<a<<endl;
    return 0;
}


-->
hello world 
(3,4)

其中,思考一下两个问题:

  1. operator<< 返回的是左值引用。但什么是左值引用呢?

    可以实现连续赋值的操作,如==,+=,<<等。这里就是说,cout<<a之后仍返回一个ostream的实例。

    更简单的解释是:下面这个例子

    int 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)。

    小结:连续运算符的重载应返回左值引用

  2. 为什么第二个参数要为const类型?

    • 不希望对类成员变量进行修改。
    • 加上const,对于const和非const的实参,函数都能接受。但不加的话,就只能接受非const的实参。编译不能通过(报错为:error: no match for ‘operator<<’ (operand types are ‘std::ostream {aka std::basic_ostream}’ and ‘const Point’))

    同时补充一下,使用引用&Point 是因为 可以避免在函数调用时对实参的一次拷贝,提高了效率。

<工程开发规范>

对于<运算符重载>部分的代码:

存在以下问题:

  1. 没有将类的声明与定义分离
  2. 暴露了类的成员变量
  3. 命名空间的使用,不清楚到底引入了多少符号!

修改如下:

#include<iostream>
//using namespace std;
using std::cout;
using std::endl;
using std::ostream;

class Point{
public:
    Point(int x,int y);
    friend ostream &operator<<(ostream &out,const Point &p);    // 若私有变量需要被外部函数使用,那么可以使用友元函数。
private:        // 保护类的成员变量
    int x,y;    
};

Point::Point(int x,int y):x(x),y(y){}    // 分离类的声明与定义

//重载运算符
ostream &operator<<(ostream &out, const Point &p){
    out<<"("<<p.x<<","<<p.y<<")";
    return out;
}

int main(){
    Point a(3,4);
    cout<<"hello world"<<endl;
    cout<<a<<endl;
    return 0;
}

但是注意,类的成员变量若是私有,外部需要访问就需要将 使用了这个私有成员变量的函数 声明为友元函数。

<c++为什么难学>

有以下几个原因:

  • c++语法难学,多样复杂

  • “技术想象力”要求高(用“编程范式”来举例了,如:面向过程,面向对象,泛型编程,函数式编程。即设计代码的思想),来源于夯实的基础知识。为什么会看不懂开源代码?就是由于基础知识的匮乏导致的技术想象力有限

    技术想象力的例子,以c++中实现加法为例:

    // 面向过程
    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源码变成可执行文件的重点的两个过程(一共有6个过程):

在这里插入图片描述

对象文件中,存放的是“定义

多个C源码被编译为多个对象文件,然后将多个对象文件链接成一个可执行文件。

<声明与定义>

  • 声明:相当于一个说明的作用
  • 定义:表示具体的实现过程
// 声明
int add(int a, int b);

// 定义
int add(int a,int b){
	return a+b;
}

对于对象文件是什么感到好奇吗?

<编译阶段>

编译阶段的主要目的:语法检查。即确保c源码没有语法错误,能生成对象文件。

只执行编译的命令:

g++ -c main.cpp

查看对象文件main.o中到底是什么东西:

nm -C main.o

得到以下的结果:

在这里插入图片描述

其中,前面有一串数字以及T字母的 这些方法是当前文件中定义的;没有数字的 表示那些方法的定义是在外部进行查找的。

思考:那些需要外部查找定义的函数是如何被找到的呢?

注意:编译阶段不需要知道所声明函数的定义

<链接阶段>

链接阶段的主要目的:将多个对象文件拼接在一起。把对象文件中需要外部查找的方法与其具体的定义链接起来。

相当于,你有病(需要外部查找的方法),我有药(方法的定义),我们之间匹配上了。

举例:

(1)main.cpp --> main.o

#include<iostream>
using namespace std;

int add(int a,int b);

int main(){
    cout<<"add(3,4)="<<add(3,4)<<endl;
    return 0;
}

// main.o的部分内容
                 U add(int, int)
0000000000000000 T main

(2)add.cpp --> add.o

int add(int a,int b){
    return a+b;
}

// add.o中的内容
0000000000000000 T add(int, int)

接着将这两个对象文件连接在一起,就可以形成一个完整的可执行程序。

g++ main.o add.o

然后执行这个可执行文件

./a.out

-----result------
add(3,4)=7

<小结>

  1. 编译阶段主要在做 语法检查, 而链接阶段主要在做 定义匹配
  2. 声明作用于编译阶段,帮助源程序通过编译(编译期间出现的错误主要有:符号未定义/找不到,语法错误等);定义作用在链接阶段,帮助对象文件完成定义的匹配(链接阶段出现的错误主要有:定义不存在或者定义冲突)
  3. 声明主要放在头文件中,定义主要放在源文件中

google测试框架

重点:学习如何在c/c++工程中引入第三方功能模块

我们在编译阶段只需要第三方模块的声明(在头文件.h中),在链接阶段,我们需要第三方模块的定义(静态链接库.a文件)

googletest提供的是cmake方法进行编译,那么新建build文件夹并对googletest模块进行编译,得到如下结果:

zzmine@ubuntu:~/myLibs/googletest/build$cmake ../
zzmine@ubuntu:~/myLibs/googletest/build$ make
Scanning dependencies of target gtest
[ 12%] Building CXX object googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
[ 25%] Linking CXX static library ../lib/libgtest.a
Scanning dependencies of target gmock
[ 37%] Building CXX object googlemock/CMakeFiles/gmock.dir/src/gmock-all.cc.o
[ 50%] Linking CXX static library ../lib/libgmock.a
Scanning dependencies of target gmock_main
[ 62%] Building CXX object googlemock/CMakeFiles/gmock_main.dir/src/gmock_main.cc.o
[ 75%] Linking CXX static library ../lib/libgmock_main.a
[ 75%] Built target gmock_main
Scanning dependencies of target gtest_main
[ 87%] Building CXX object googletest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
[100%] Linking CXX static library ../lib/libgtest_main.a

注意这里生成了4个.a文件,并且有一个lib文件夹,这个就是第三方模块googletest中方法的定义!

然后在xxx/googletest/googletest/ 中可以找到include文件,这就是第三方模块googletest中方法的声明!

接着就可以使用googletest编写C++程序了:

#include<iostream>
#include<gtest/gtest.h>
using namespace std;

int add(int a,int b){
    return a+b;
}

TEST(test,add1){
    EXPECT_EQ(add(3,4),8);
    EXPECT_EQ(add(3,4),7);
}

TEST(test,add2){
    EXPECT_LT(add(3,4),7);
    EXPECT_LT(add(3,4),8);
}

int main(){
    cout<<"add(3,4)="<<add(3,4)<<endl;
    return RUN_ALL_TESTS();
}

但是,在执行编译过程时,发生以下报错:

zzmine@ubuntu:~/CPractice$ g++ -c main.cpp
main.cpp:2:9: fatal error: gtest/gtest.h: No such file or directory
 #include<gtest/gtest.h>
         ^~~~~~~~~~~~~~~
compilation terminated.

说明,其实在默认的头文件搜索路径中,没有包括googletest的include文件夹。

所以需要告诉g++编译器,在编译搜索目录时需要加上googletest的include路径。执行(这里我把googletest的include路径拷贝到当前目录中了):

g++ -I./include -c main.cpp 

完成了编译过程,此时在当前目录中生成了一个main.o文件。用nm -C main.o查看对象文件内容,发现有大量的外部符号链接。

接下来进行链接过程,将main.o与之前生成的googletest静态链接库文件进行链接。

g++ -L./lib main.o -lgtest

最后,执行可执行文件 ./a.out 获得想要的输出结果。

预处理

C++源文件通过预处理得到待编译源码,待编译源码决定了可执行文件的功能

所有预处理命令都是以#开头,其中宏定义是预处理家族中的一员,宏做的事情就是简单的替换。

<宏定义>

  • 定义符号常量

    #define PI 3.1415926
    #define MAX_N 10000
    
  • 定义表达式

    #define S(a,b) a*b
    
  • 定义代码段:注意,每行后面要加\ 表示连接,因为预处理命令只能写在一行

    #define P(a) { \
    	printf("%d\n",a); \
    }
    

备注:#include 表示将后面库文件的内容添加到当前文件中。

举例:define.cpp

#include<iostream>
using namespace std;

#define PI 3.1415926

int main(){
    printf("%lf\n",2*PI);
    return 0;
}

可以执行一下命令来查看待编译源码的内容:

g++ -E define.cpp

也可以通过重定向文件输出到某个文件中进行查看:

g++ -E define.cpp > output.cpp

发现主函数部分被替换成了如下的代码:

// ... 上面省略了很多产生的内容

int main(){
    printf("%lf\n",2*3.1415926);
    return 0;
}

需要特别注意的是,可执行文件的功能完全取决于待编译源码!

可以举出下面的一些例子:

#define S(a,b) a*b

S(3,4)        --->    3*4=12
S(3+9,4)      --->    3+9*4=39
int n = 5;
S(int,p)=&n;  --->    int*p = &n;

<预定义的宏>

说明
__ DATE __日期:Mmm dd yyyy
__ TIME __时间:hh:mm:ss
__ LINE __行号
__ FILE __文件名
__ FUNC __函数名/非标准
__ PRETTY_FUNCTION __更详细的函数信息

反思

通过这种探索式的学习,自己发现其实C++中有很多有趣的内容,而这些在之前的课程学习中完全被忽略掉了,当时的我只是很功利地去模拟书上的代码,却很少静下心来领略编程的艺术。

同时,我在重学C++的过程中,全程使用的是Linux平台的g++编译器以及vim编辑器,而没有选择更加“方便”的IDE。在学习的过程中,我对c++文件从文本到可执行文件的过程有了更深的理解。我明显地感觉到自己对C++的理解更加深刻了,其实这种感觉也说不上来是什么样子,大概就是代码打起来手比较熟,报错啥的完全不慌哈哈哈哈哈。印象最深刻的就是可执行文件的功能其实完全取决于预编译源码,而并不是自己写的源代码。感觉正确的编程学习方式还是要基于原理,然后大规模开发需要提升效率再使用IDE。

最后,这并不是一周所学的全部内容,只是这些都是C++预热的部分,而且全放在一篇文章会显得很臃肿,所以其余内容将会在后续发布。

若有任何疑问,欢迎交流!

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值