学 c++ 之前,我主要用过的编程语言有 java/php/go/js/python,这些语言语法上比较简单,基本上 1个月以内就能够达到比较熟悉的程度。而且这几门语言都有很多相似之处,学起来容易理解。
学习 c++ 的过程就比较 “痛苦”:我断断续续,花了6个月时间,才从头到尾把英文版的 《C++ Primer》看了一遍。整个心路历程很复杂。
总结下来,c++ 相对于其他高级语言来说,有以下难点:
下面基于这几点展开来交流。
以指针和引用为例,这两个在其他编程语言里不常见。
指针本身并不复杂,但指针和其他乱七八糟的东西配合起来使用,就很容易迷惑新手。
比如指针与常量:下面这两行代码有什么错误?
const int a = 1;
int * const pa = &a;
熟悉的同学一眼就能看出:int * const pa 声明的 pa 是个常量,它指向的内容 int 不是常量,而 const int a 是常量,所以 a 的地址不能赋值给 pa。
简单修改以下就可以了:
const int * const pa = &a;
java、php 里都有引用一说:函数传参时,基础类型是值传递,对象/数组 类型是引用传递。但在 c++ 里,我才认识到什么是引用。
引用的定义也很简单:引用变量是一个别名,某个已存在变量的另一个名字。
int a = 1;
int& b = a; // 引用的初始化(这里的 等于号不能叫赋值)
int c = 5;
b = c; // 赋值
cout << a << endl; // 5
当我看到右值引用的时候,我就觉得 “有点儿意思” 了。当然其他语言里也会有右值,但在编程的时候体现不出来,感受不到右值的存在。
int&& b = getValue(); // 一个右值引用的定义与初始化
根据以下规则可以判断表达式返回的是左值还是右值。
左值:返回左值的表达式有:
右值:返回右值的表达式有:
其中:
左值是持久的,右值是临时的。右值引用是左值。
c++ 在资源管理上有 move assign 的操作,即如果把右值赋值给其他对象,可以把右值里的资源 move 到新的对象里,这样防止进行资源的申请与释放。
c++ 的面向对象语法也比较复杂,比如 为了使资源管理更高效的一系列 copy control 函数,继承权限控制,多继承,以及为了弥补多继承问题而设计的虚继承等。
《C++ Primer》 里专门有一章,叫 Copy Control,介绍如何定义、使用:复制构造、复制赋值、移动构造、移动赋值等。
复制构造与复制赋值的定义:
ClassName::ClassName(const ClassName&); // 构造函数没有返回值
ClassName & ClassName::operator=(const ClassName &); // 复制赋值,返回 *this
移动构造,移动赋值的定义:
ClassName::ClassName(ClassName &&) noexcept; // 使用右值移动构造。move construct 不能抛出异常
ClassName &ClassName::operator=(ClassName &&) noexcept; // 不能抛出异常
一个移动赋值的例子:
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 成员,那么在外面也不能直接访问子类对象的此成员。
class Child : private Parent {
// ...
}
如果派生描述符不是 public,则子类的指针、引用 不能隐式转换为父类的指针、引用。
protected 成员的逻辑跟其他语言也有所不同:
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 的成员。
虚继承例子:
class A1 : public virtual A {...}
class A2 : public virtual A {...}
class B : public A1, public A2 {...}
c++ 的泛型与 java 的泛型有所不同,实现上:
所以在运行性能上,c++ 的模板性能要高于 java;在编译代码结果上,c++ 泛型编译出来的代码量要远大于 java。
普通的模板函数:
template<typename T>
bool comp(const T &t1, const T &t2)
{
return t1 < t2;
}
// 使用时
cout << comp<int>(1, 2) << endl; // 1 (true)
值类型参数模板:
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)
当值作为参数模板时,值必须时常量表达式。
模板类与类里的模板方法的定义互不影响:
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 关键字声明:不要在这个文件生成特例化代码:
extern template declaration; // 模板实例化声明
有时候,我们在使用模板的类型 T 时,想要返回特定的与 T 相关的类型时,会需要用到 type_traits 里提供的一系列模板工具。
比如,编译器推断 T 为 X& 时(X 指的是纯类型),我们可以用 remove_reference<T> 得到 X。
这些类型操作模板有:
STL 里提供了大量的模板函数和模板类。写出能够合理利用这些库的代码,能够提升代码效率,同时让工作变得更轻松。
容器类都实现了右值引用的 move insert 操作,比如 vector::push_back 实现的有 push_back(value_type&&) 版本:
void push_back(value_type&& __x)
{ emplace_back(std::move(__x)); }
当我们的值是右值时,push_back 会调用 move construct 来提升性能。
算法库里提供了大量的常用算法:
这些算法的参数都是 迭代器。基础的迭代器可以分为五种:
比如,forward_list 的迭代器就是 forward iterator,而 list 的迭代器是 bidirectional iterator。
根据算法所使用的迭代器类型,来合理的规划我们的代码实现。
以上是我对于 c++ 语言上的几处难点的总结。
c++ 的难也绝不仅仅是上述那么几点。c++ 是更接近系统底层的语言,想要使用的得心应手,还需要 操作系统、计算机组成原理、linux、网络 等方面的知识有全面的了解;以及 STL 库,Boost 库等等。这一定是长期持续不断的学习过程,好好享受。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。