1. 再论左值与右值
以前写过关于左值和右值的两篇总结,这里还需要再补充一点。那天在知乎看到Milo大神的这样一个回答:知乎。
自己也去查了文档,cppreference也清楚地指出:赋值操作在C++和C语言中确实是不一样的,在C++中返回左值(而且是引用)。
所以(a = b) = 1这样的表达式在C语言中是编译错误的,在C++中却可以编译成功,正如Milo大神如是说:因为 C++ 提供了引用及运算元重载,才可以返回更方便的左值。
C++和C语言中的左值和右值的概念其实并无太大区别,只是C++11中为了引入右值引用,新添加了一个将亡值(expiring value)而已,而C++中的纯右值其实是C
语言中的右值。如下图:
左值表示持久,右值表示临时。位于赋值号左侧和右侧从来不是左值和右值的判断标准,能不能以 & 运算符取地址才是判断的唯一标准。
典型的右值有中间计算量,函数返回的临时对象,字面值常量(除字符串)等,这些cppreference都有明确地写到。
下面再来看这样一段代码:
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 int main() 6 { 7 string s1 = "Hello"; 8 string s2 = "world"; 9 10 cout << (s1 + s2 = "OK") << endl; 11 12 int i = 0; 13 int j = 20; 14 15 //cout << (i + j = 30) << endl; //编译不通过 16 }
在以前那篇随笔中我也记得在primer中看到过,可以对 s1 + s2这样的右值赋值,当时觉得就是为了兼容旧标准也没多想。但是却不能对 i + j 这样的临时对象赋值。
其实因为string类重载了赋值运算符,所以 s1 + s2 = "OK"就相当于 (s1 + s2).operator=("OK"),是以一个右值来调用成员函数的行为,C++是允许这样做的,虽然没有
太大意义,因为 s1 + s2 这个临时变量在分号之后就不存在了。
C++ 11下规定可以禁止这种行为,就是不允许右值来调用成员函数。
1 class Foo 2 { 3 void callByLvalueOnly() & // & 限定该成员函数只能被左值调用 4 { 5 } 6 7 void callByRvalueOnly() && // && 限定该成员函数只能被右值调用 8 { 9 } 10 11 Foo & operator=(const Foo&) & // 限定了只能对左值进行赋值 12 { 13 } 14 };
具体内容可以参考知乎下的这个问题:知乎。
2. 再论Java和C++中的static
以前写过这样的一篇博客:Java和C++中的static,那时候理解的还非常浅。
Java中的 static 表示的是类成员或者类方法,其中有一种代码块叫静态代码块,静态代码块以及静态成员的初始化是在类加载的时候就会进行的(其实类初始化是类加载的
最后一步)。
而Java中有独特的运行时动态加载特点,只有在用到这个类的时候才会进行加载。常见的可以触发类加载的有:调用类的静态方法/静态属性,实例化类的对象等。
根据Java虚拟机规范,主动触发类加载的行为主要有以下六种:
1) 创建类的实例。
2) 访问某个类或者接口的静态变量,或者对该静态变量赋值(如果访问静态编译时常量(即编译时可以确定值的常量)不会导致类的初始化)。
3) 调用类的静态方法。
4) 反射(Class.forName(xxx.xxx.xxx))。
5) 初始化一个类的子类(相当于对父类的主动使用),不过直接通过子类引用父类元素,不会引起子类的初始化。
6) Java虚拟机被标明为启动类的类(包含main方法的)。
类与接口的初始化不同,如果一个类被初始化,则其父类或父接口也会被初始化,但如果一个接口初始化,则不会引起其父接口的初始化。参见:Java系列笔记。
但是C++不同,C++语言没有运行时的动态类加载机制。C++的类中的静态变量是在运行时就得到内存分配的,存储在全局/静态变量区,而且会被默认初始化。(int 为 0)
静态变量的存在与类有没有被“加载”没有关系,C++没有动态类加载机制。
3. Thinking in Java中的一个错误
《Java编程思想》第96页描述的:“即使没有显式地使用static关键字,构造器实际上也是静态方法”。这句话是作者的主观认识,是完全错误的。
构造器是毫无疑问的实例方法,而且是非常特殊的实例方法(没有返回值、无法通过 . 来调用等),构造器的参数列表中会有被隐式传入的this。
然后构造器作为一种实例方法,是静态分派的(statically dispatched),也就是不是运行时绑定的,无法参与运行时多态。这一点上和private、final修饰的实例方法相同,
都是实例方法,却都是静态分派的,无法参与运行时多态。事实上,private方法就是隐式final的,这点TIJ上倒是没有说错。
具体参考知乎R大神的详细答案:知乎。
4. 关于头文件
轮子哥在知乎说:只要不会引起编译错误,并且不会要求你要使用前置声明的话,那尽量放在cpp里面总是不会错的。只有很少数的情况下你需要考虑要不要放进头文件里,
譬如说inline函数,或者static const int变量等。
实际编码设计过程中,一个原则就是在类的头文件中最好不要包含其他头文件,因为这样会使类之间的文件包含关系变得复杂化。
注意使用前向声明。一定不要在头文件中使用using namespace。
参考:注意头文件规则。
5. 模板不能实现分离式编译
C++的模板template不能实现分离式编译,通常都是将模板的声明与实现都放在头文件中,标准库都是这样做的。编译器很聪明,针对模板这种行为,不会引起重定义的错误。
关于原因,具体参见刘未鹏大神的文章:为什么C++编译器不能支持模板的分离式编译。
6. 编译链接和运行
编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,主要包括预处理、编译、优化等主要阶段。
链接过程是由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。 例如,某个源文件中的函数可能引用了另一个源文件
中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
运行就是让程序跑起来,是唯一的存在内存分配和回收的过程。
7. 关于C++中的命名空间
命名空间其实也是一种作用域,和类、函数、块甚至文件是一样一样的。
通常也是在头文件中定义命名空间,然后在cpp文件中实现时需要用作用域操作符 ::来限定哪个命名空间。命名空间的详细说明:MSDN。
当然也可以像在平时写demo一样时在 main.cpp 下定义并实现一个类一样,来定义命名空间。
8. 析构函数
以前说的很清楚,C++编译器只有在需要提供默认构造函数的时候,才会提供合成的默认构造函数。而需要的情况就是以前总结出来的五种情况:
父类有默认构造函数,类的数据成员是别的具有默认构造函数的类实例,虚函数,虚继承,以及类内初始化。
而对于析构函数,不能有参数不能重载,只有前两种情况会决定会不会合成析构函数:即父类有默认析构函数,类的数据成员是别的有默认析构函数的类实例。后三种
情况跟析构函数没有半毛钱关系。
五大函数加上构造函数这六大函数都是不能被继承的。
运算符重载的函数都是能够被继承的,除了赋值操作符。