前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >吃透这些内容,c++ 不再难学

吃透这些内容,c++ 不再难学

原创
作者头像
祥祥
修改2021-01-07 17:57:15
1.3K0
修改2021-01-07 17:57:15
举报
文章被收录于专栏:大写的CPP大写的CPP

学 c++ 之前,我主要用过的编程语言有 java/php/go/js/python,这些语言语法上比较简单,基本上 1个月以内就能够达到比较熟悉的程度。而且这几门语言都有很多相似之处,学起来容易理解。

学习 c++ 的过程就比较 “痛苦”:我断断续续,花了6个月时间,才从头到尾把英文版的 《C++ Primer》看了一遍。整个心路历程很复杂。

总结下来,c++ 相对于其他高级语言来说,有以下难点:

  • 基本语法
  • 面向对象编程逻辑
    • 复制、移动
    • 多继承
  • 模板编程复杂
  • 如何写出与 STL 紧密协作的代码

下面基于这几点展开来交流。

复杂的基础语法

以指针和引用为例,这两个在其他编程语言里不常见。

指针本身并不复杂,但指针和其他乱七八糟的东西配合起来使用,就很容易迷惑新手。

比如指针与常量:下面这两行代码有什么错误?

代码语言:c++
复制
const int a = 1;
int * const pa = &a;

熟悉的同学一眼就能看出:int * const pa 声明的 pa 是个常量,它指向的内容 int 不是常量,而 const int a 是常量,所以 a 的地址不能赋值给 pa。

简单修改以下就可以了:

代码语言:c++
复制
const int * const pa = &a;

java、php 里都有引用一说:函数传参时,基础类型是值传递,对象/数组 类型是引用传递。但在 c++ 里,我才认识到什么是引用。

引用的定义也很简单:引用变量是一个别名,某个已存在变量的另一个名字。

代码语言:c++
复制
int a = 1;
int& b = a;     // 引用的初始化(这里的 等于号不能叫赋值)
int c = 5;
b = c;          // 赋值
cout << a << endl;   // 5

当我看到右值引用的时候,我就觉得 “有点儿意思” 了。当然其他语言里也会有右值,但在编程的时候体现不出来,感受不到右值的存在。

代码语言:c++
复制
int&& b = getValue();  // 一个右值引用的定义与初始化

根据以下规则可以判断表达式返回的是左值还是右值。

左值:返回左值的表达式有:

  • 返回左值引用的函数返回值
  • 赋值表达式
  • 下标表达式
  • dereference 表达式
  • ++ -- 前缀

右值:返回右值的表达式有:

  • 返回非引用类型的函数返回值
  • 算数表达式
  • 关系表达式
  • 位运算
  • 后缀 ++ --

其中:

  • 左值引用可以引用左值
  • 右值引用、const 左值引用 可以引用右值

左值是持久的,右值是临时的。右值引用是左值。

c++ 在资源管理上有 move assign 的操作,即如果把右值赋值给其他对象,可以把右值里的资源 move 到新的对象里,这样防止进行资源的申请与释放。

面向对象编程

c++ 的面向对象语法也比较复杂,比如 为了使资源管理更高效的一系列 copy control 函数,继承权限控制,多继承,以及为了弥补多继承问题而设计的虚继承等。

《C++ Primer》 里专门有一章,叫 Copy Control,介绍如何定义、使用:复制构造、复制赋值、移动构造、移动赋值等。

复制构造与复制赋值的定义:

代码语言:c++
复制
ClassName::ClassName(const ClassName&);   // 构造函数没有返回值
ClassName & ClassName::operator=(const ClassName &);         // 复制赋值,返回 *this

移动构造,移动赋值的定义:

代码语言:c++
复制
ClassName::ClassName(ClassName &&) noexcept;   // 使用右值移动构造。move construct 不能抛出异常
ClassName &ClassName::operator=(ClassName &&) noexcept;  // 不能抛出异常

一个移动赋值的例子:

代码语言:c++
复制
StrVec &StrVec::operator=(StrVec &&rhs) noexcept {
  // 当前对象如果是 rhs,则不处理,直接返回
  if (this != &rhs) {
    free(); // 自定义方法,释放当前对象的资源
    elements = rhs.elements; // 从 rhs 里接手其他资源(一般是指针)
    first_free = rhs.first_free; cap = rhs.cap;
    // 把 rhs 里的资源设置为空
    rhs.elements = rhs.first_free = rhs.cap = nullptr; 
  } 
  return *this; 
}

c++ 里的继承存在继承权限的设置(派生描述符),比如 如果是 private 继承,则尽管在父类里是 public 成员,那么在外面也不能直接访问子类对象的此成员。

代码语言:c++
复制
class Child : private Parent {
// ...
}

如果派生描述符不是 public,则子类的指针、引用 不能隐式转换为父类的指针、引用。

protected 成员的逻辑跟其他语言也有所不同:

  • protected 可以被子类访问
  • 可以被 friend 访问
  • 可以被子类修改权限描述符
代码语言:c++
复制
class A {
protected:
    int t;
};

class B : public A {
public:
    using A::t;  // B 里能访问到 A::t,就可以在这里把 t 改为 public 成员
};

c++ 支持多继承,实例化的子类对象里包含了 子类成员部分+父类成员部分。当 B 继承自 A1,A2,A1,A2 继承自 A 的时候,一般情况下 B 的实例会有两块 A 的成员部分。A1,A2 的操作访问的 A 的成员不是同一个,这可能导致很多问题。所以就有了虚继承,虚继承可以保证 B 的实例只有一份 A 的成员。

虚继承例子:

代码语言:c++
复制
class A1 : public virtual A {...}
class A2 : public virtual A {...}
class B : public A1, public A2 {...}

面向模板编程

c++ 的泛型与 java 的泛型有所不同,实现上:

  • java 泛型原理是编译时类型擦除,比如把 T 编译为 Object,然后运行时动态转换类型
  • c++ 模板原理时编译时特例化,根据模板类、模板函数,编译生成对应类、函数的代码

所以在运行性能上,c++ 的模板性能要高于 java;在编译代码结果上,c++ 泛型编译出来的代码量要远大于 java。

普通的模板函数:

代码语言:c++
复制
template<typename T>
bool comp(const T &t1, const T &t2)
{
    return t1 < t2;
}

// 使用时
cout << comp<int>(1, 2) << endl;   // 1 (true)

值类型参数模板:

代码语言:c++
复制
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
    return strcmp(p1, p2);
}
cout << compare("h1", "ff") << endl;   // 1 (true)

当值作为参数模板时,值必须时常量表达式。

模板类与类里的模板方法的定义互不影响:

代码语言:c++
复制
template<typename T>            // 模板类
class A {
public:
    A() {};
    T getValue(const T &t) const { return t; }
    template<typename U> U getUValue(const U &u) { return u; }        // 模板方法
};

// 使用示例
A<int> a;
cout << a.getValue(10) << endl;
cout << a.getUValue<string>("hello") << endl;

在相同的文件里,多次使用相同的特例化模板时,只会特例化一份代码。而在不同的文件里,如果都是用了上面的 A<int>,则会在各自文件声称自己的特例化代码,这在大型系统中的代码开销是不可接受的。

可以使用 extern 关键字声明:不要在这个文件生成特例化代码:

代码语言:c++
复制
extern template declaration;   // 模板实例化声明

有时候,我们在使用模板的类型 T 时,想要返回特定的与 T 相关的类型时,会需要用到 type_traits 里提供的一系列模板工具。

比如,编译器推断 T 为 X& 时(X 指的是纯类型),我们可以用 remove_reference<T> 得到 X。

这些类型操作模板有:

  • remove_reference
  • add_const
  • add_lvalue_reference
  • add_rvalue_reference
  • remove_pointer
  • add_pointer
  • make_signed
  • make_unsigned
  • remove_extend X[n] -> X
  • remove_all_extend X[n][m] -> x

写出与 STL 紧密协作的代码

STL 里提供了大量的模板函数和模板类。写出能够合理利用这些库的代码,能够提升代码效率,同时让工作变得更轻松。

容器类都实现了右值引用的 move insert 操作,比如 vector::push_back 实现的有 push_back(value_type&&) 版本:

代码语言:javascript
复制
void push_back(value_type&& __x)
      { emplace_back(std::move(__x)); }

当我们的值是右值时,push_back 会调用 move construct 来提升性能。

算法库里提供了大量的常用算法:

  • 查找
  • 排序
  • 合并
  • 替换
  • ...

这些算法的参数都是 迭代器。基础的迭代器可以分为五种:

  • input iterator:只读,不能写,只能增加
  • output iterator:只写,不读,只能增加
  • forward iterator:可读写,只能增加
  • bidirectional iterator:可读写,能增加、减少
  • random-access iterator:可读写,可随机访问,支持下标操作( iter[n], *(iter + n) )

比如,forward_list 的迭代器就是 forward iterator,而 list 的迭代器是 bidirectional iterator。

根据算法所使用的迭代器类型,来合理的规划我们的代码实现。

总结

以上是我对于 c++ 语言上的几处难点的总结。

c++ 的难也绝不仅仅是上述那么几点。c++ 是更接近系统底层的语言,想要使用的得心应手,还需要 操作系统、计算机组成原理、linux、网络 等方面的知识有全面的了解;以及 STL 库,Boost 库等等。这一定是长期持续不断的学习过程,好好享受。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 复杂的基础语法
  • 面向对象编程
  • 面向模板编程
  • 写出与 STL 紧密协作的代码
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档