最近因为某些原因决定重新开始学习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++的坑需要学习很多东西。不管怎么说,一点一点积累,把基础打扎实最重要。