细节问题(三)

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++编译器只有在需要提供默认构造函数的时候,才会提供合成的默认构造函数。而需要的情况就是以前总结出来的五种情况:

   父类有默认构造函数,类的数据成员是别的具有默认构造函数的类实例,虚函数,虚继承,以及类内初始化。

   而对于析构函数,不能有参数不能重载,只有前两种情况会决定会不会合成析构函数:即父类有默认析构函数,类的数据成员是别的有默认析构函数的类实例。后三种

   情况跟析构函数没有半毛钱关系。

   五大函数加上构造函数这六大函数都是不能被继承的。

   运算符重载的函数都是能够被继承的,除了赋值操作符。

   

原文地址:https://www.cnblogs.com/niuxichuan/p/5994511.html