最近因为某些原因决定重新开始学习C++。考虑到自己在大学里面学到的C++有点旧(估计是C++98),所以打算从C++11开始。
C++11如其名,是2011年出来的标准,所以2011年之后才有编译器实现。现在2019年大部分PC以及服务器应该都支持C++11了。
个人习惯于看书来学习某样东西,所以找了C++相关书的资料。一开始在o’reilly上找,发现很多书都比较旧。虽然有Effective C++以及More Effective C++系列,但对于初学者来说还不是时候。最后在Stackoverflow上找到了一个比较全的推荐书列表
https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list
比如面向初学者
- 《C++ Primer》(The fifth edition (released August 16, 2012) covers C++11)
- 《Programming: Principles and Practice Using C++》 (Bjarne Stroustrup, 2nd Edition – May 25, 2014)
面向有编程经验的人
- 《A Tour of C++》
- 《Accelerated C++》
个人最后选择了可能是最全的C++作者写的《The C++ Programming Language》。原因也很简单,希望全面地,与时俱进地学习C++11。
到现在,看了100多页(总共有1300多页),不得不说,C++11和大学学的有很大不同,以及C++和C最好还是分开来看,C的编程理念和C++还是有所不同的。
具体到实际代码,我想以
std::string str = "foo";
为例子展开一下。
首先,右边的string literal,类型是const char*,和左边的std::string不同。那么string literal被转换成了string么?
直观上可以怎么理解,但背后不是这样。这里“转换”的前提是string有一个参数是const char*的构造函数。
考虑自己写一个StringLiteral类,设计一个以const char*类型为参数的构造函数。
// immutable
class StringLiteral {
public:
// don't add explicit here
StringLiteral(const char *chars) {
std::cout << "StringLiteral(const char*)\n";
length_ = std::strlen(chars);
chars_ = chars;
}
// copy
// StringLiteral(const StringLiteral &liternal) = delete;
StringLiteral(const StringLiteral &literal) {
std::cout << "StringLiteral(copy)\n";
length_ = literal.length_;
chars_ = literal.chars_;
}
// copy assignment not allowed
StringLiteral &operator=(const StringLiteral &) = delete;
// move
// StringLiteral(StringLiteral &&) = delete;
StringLiteral(StringLiteral && literal) {
std::cout << "StringLiteral(move)\n";
length_ = literal.length_;
chars_ = literal.chars_;
};
// move assignment not allowed
StringLiteral &operator=(StringLiteral &&) = delete;
size_t length() const { return length_; }
char char_at(int i) const {
if(i < 0 || i >= length_) {
throw std::out_of_range{"out of range"};
}
return chars_[i];
}
friend std::ostream& operator<<(std::ostream& os, const StringLiteral& literal) {
for(int i = 0; i < literal.length_; ++i) {
os << literal.chars_[i];
}
return os;
}
~StringLiteral() {
std::cout << "~StringLiteral()\n";
}
private:
size_t length_;
const char *chars_;
};
int main() {
StringLiteral sl1 = "foo";
std::cout << sl1 << std::endl;
return 0;
}
执行上述程序,你可以发现输出了以下内容
StringLiteral(const char*) foo ~StringLiteral()
也就是说,构造函数被调用了。这不是碰巧试出来的特性,而是一个叫做converting constructor的东西。简单来说,如果你构造函数上没有加explicit的话,就可以作为converting constructor。换句话说,如果上述程序中,构造函数前加了explicit(注释don’t add explicit here),那么程序就无法编译。加了explicit的话,StringLiteral必须以类似下面这种显示的方法构造
StringLiteral sl1{"foo};
回到没有explicit的程序的输出。从程序输出来看,似乎仅仅是StringLiteral的构造函数被调用了,又考虑到StringLiteral不需要被move,那么是否可以把StringLiteral的move constructor给delete掉?
答案是不可以(这里个人觉得是C++一个很不直观,或者说坑人的地方,虽然move构造函数没有被调用但是不允许删除)。move函数不能被删除的理由,个人理解是,可能部分编译器不支持elide-constructor(构造函数消除)优化。
同样是上述程序,在编译的时候,加入 -fno-elide-constructors 标志,即取消构造函数消除优化
g++ main.cpp -std=c++11 -fno-elide-constructors -o string_liternal
再次执行时,你可以看到以下输出
StringLiteral(const char*) StringLiteral(move) ~StringLiteral() foo ~StringLiteral()
似乎和之前的结果不一样?实际编译器做的事情类似
StringLiteral temp{"foo"};
StringLiteral stl1{std::move(temp)};
如果你高兴,你还可以注释掉move constructor,看下结果
StringLiteral(const char*) StringLiteral(copy) ~StringLiteral() foo ~StringLiteral()
从move constructor变成了copy constructor。实际代码类似
StringLiteral temp{"foo"};
StringLiteral sl1{temp};
这里到底发生了什么?
答案在这里。
再看一次
std::string str = "foo";
在C++里这其实触发了copy-initialization。具体来说内部使用converting constructor构造了一个临时string变量,然后使用copy constructor再次构造了str。注意这里并不会调用copy assignment构造函数(上面的程序中copy/move assignment都delete掉了,间接证明了不会被调用)。
似乎没有move constructor的事情?介绍copy initialization的页面中有一句
If other is an rvalue expression, move constructor will be selected by overload resolution and called during copy-initialization. There is no such term as move-initialization.
也就是说,如果有用户定义了的move constructor的话,会优先使用move constructor。
至此,为什么输出会有copy/move构造函数都可以理解了。
当然,这里明显有一次多余的构造,所以编译器会执行构造消除优化。
小结
老实说C++很难,从上面这一个语句背后需要理解的概念就说明完全理解C++,或者说避免C++的坑需要学习很多东西。不管怎么说,一点一点积累,把基础打扎实最重要。