C++ 面向对象特性杂记: 构造函数, 虚函数, 重载, 重写

前言

重读了C++ Primer, 记录和整理面向对象的一些特性, 包括构造函数, 默认构造, 虚函数, 析构, 重写(override), 重载(overload)等特性, 写了一点测试代码验证了自己的一些总结.

C++构造函数(重载, 默认实参, 默认构造函数的注意点)

1) 默认构造函数最好明确指定, 如果没有的话, 编译器会给你补充一个, 应当尽量避免, 因为由编译器给你补充的这个默认构造函数的行为未必一定如你所愿.

2) 默认构造函数在对象变量申明(包括动态申请内存的情况和一般的变量申明如下所示)的时候没有带初始化式的情况下调用, 比如下面的代码片段, 参见注释, 值得注意的是对于类类型的对象动态申请内存的时候的调用默认构造函数的语法结构特别需要数以的, 也就是可以使用空括号也可以不适用空括号, 两者没有区别, 但是对于基本类型, 这个就牵涉到初始化不初始化的问题了. 参见下面的注释.

class Human {
private:
    string name;
public:
    Human(): name("Default") {}
};
Human h; // call the default constructor
Human *ptr = new Human; // call default constructor
Human *ptr = new Human();  // same effect as above

int *pi = new int;   // pi points to an uninitialized int
int *pi = new int(); // pi points to an int valued initialized to 0

默认构造函数与带默认实参的构造函数之间的歧义以及注意点区分测试

Default Constructor vs constructor with default parameters: 写了一下下面这个简单的测试代码, 对应有下面六条测试结果和相关结论.

#include
#include

class Human {
    private:
        std::string name;
    public:
       Human() : name("DefaultConstructor") {
            std::cout << "Default Constructor!" << std::endl;
        }
        Human(std::string nameSpecified="ParameterDefault") : name(nameSpecified)  {
            std::cout << "Constructor with Default Parameter!" << std::endl;
        }
        virtual ~Human() {
            std::cout << "Destructor!" << std::endl;
    }
};

int main() {
    Human h1;
    return 0;
}
  1. Human h1; 编译出错, 说h1有歧义构造函数不知道调用哪个
  2. Human h1(); 没有编译出错, 也没有任何输出, 那就是调用了编译器生成的默认构造函数, 这个函数参数为空
  3. Human h1(); 不管有没有构造函数, 都可以编译通过, 即使把两个构造函数都注释掉或者注释掉其中一个, 程序运行结构都是输出为空, 也就是根本不会调用到我们自己写的构造函数, 基于这个事实, 我认为Human h1(); 这样的申明都会使用编译器自己定义的行为, 不会使用我们辨析的代码, 所以应该避免这样的申明, 具体是非缘由有待进一步考证. 也请知道各种缘由的大侠指点!
  4. Human h1; 本来是编译出错, 因为有歧义, 但是注释掉默认构造函数以后, 程序输出constructor with default parameter, 注释掉第二个构造函数就输出第一个构造函数的内容, 所以这个符合我们的预期, 使得程序按照我们编写的代码走,
  5. 另有一说(出处是谭浩强的C++书): 在一个类中定义了全部是默认参数的构造函数后,不能再定义重载构造函数, 一般不应该使用构造函数的重载和带有默认参数的构造函数, 从上面的例子中也看出来了, 切记.
  6. 所以我个人习惯是定义一个默认构造函数, 然后需要重载的就重载, 避免使用全部都是默认参数的构造函数.

C++虚函数相关(动态绑定, 非虚函数的重写等注意点)

1) 基类一律都采用纯虚析构, 原因是这样做没有任何坏处而且避免了潜在的资源释放的风险, 具体的情景是说当绑定一个基类指针到一个派生类的对象的时候, 在释放这个对象的时候如果析构函数不是虚函数, 就没有启动动态绑定, 对象释放的时候只会运行基类对象的那个析构函数, 对于派生类中的一些资源没法进行释放和清理, 这个是致命的错误.

2) 引用和指针的静态类型与动态类型可以不同, 这是C++用以支持多态性的基石

3) 另外C++ Primer有一句话: 在C++中,基类必须指定希望派生类重定义哪些函数,定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类中继承的函数不能定义为虚函数。我对这句话的理解是:

  1. 基类定义成virtual的函数是基类希望派生类重写的, 但是派生类不一定要重写, 没有重写的话, 还是调用基类中的方法, 运行时表现为基类中的这个虚函数的行为, 下面测试代码验证了这个结论, 如果把派生类Male里面的sayHi注释掉, 运行输出Human里面的sayHi内容.
  2. 基类定义成普通函数(即非虚函数, 没有开启动态绑定), 意思是没有特殊要求, 派生类可以重写, 也可以直接继承下来不重写, 如果不重写的话, 就直接集成, 无论是从基类指针还是直接从派派生类的指针调用该方法, 都只有唯一行为, 如果重写的了的话, 区别在于使用基类指针调用该方法的时候不会像虚函数那行有动态绑定, 即使通过基类指针调用, 输出的还是基类中的内容, 重写无效!这个却别在下面的代码中被验证.
class Human {
    private:
        std::string name;
    public:
        Human() : name("DefaultConstructor") {
            std::cout << "Human: Default Constructor!" << std::endl;
        }
        virtual ~Human() {
            std::cout << "Human: Destructor!" << std::endl;
        }
	virtual void sayHi() {
		std::cout << "Hi, this is base class Human!" << std::endl;
	}
	void sayGoodBye() {
		std::cout << "Bye, this is base class Human!" << std::endl;
	}
};

class Male : public Human {
	public:
		void sayHi() {
			std::cout << "Hi, this is derived class Male!" << std::endl;
		}
		void sayGoodBye() {
			std::cout << "Bye, this is derived class Male!" << std::endl;
                }
        };
        int main() {
            Human *p = new Male;
            p->sayHi();
            p->sayGoodBye();
            return 0;
         }

4) 在多啰嗦点, 重写(override)针对非虚函数也是产生效果的, 和你是不是虚函数没有直接限制或者影响, 有区别有意义的地方只发生在多态性上, 也就是使用基类指针或者引用绑定到派生类对象的时候, 这个时候才有意义去讨论重写override的行为, 换句话说就是: 虚函数关键字virtual的意思就说这段代码的行为, 是在运行时决定决定的, 重写不重写的意义区别其实只在多态发生才有意义, 也就是说用基类指针或者引用派生类对象的时候才有意义和区别, 否则就是比如上面分析的第2点, 那我定义的对象的类型是什么, 我就调用那个对象的方法就好了, 管他有没有重写, 因为只有唯一的一种可能性. 而通过基类指针调用方法则是有多重可能性多重选择的.

总结

这里主要还是复习和整理吧, 又读了C++ Primer, 记录和整理面向对象的一些特性, 包括构造函数, 默认构造, 虚函数, 析构, 重写(override), 重载(overload)等特性, 写了一点测试代码验证了自己的一些总结. 如果错误, 敬请指正!

Written on November 25, 2014