c/c++ development, inevitable custom class type (Part 4). Class and member design

Develop good class design habits

Table of contents

1. Plan member variables

        1.1 Ensuring the encapsulation of member variables

        1.2 Sharing variables with derived classes

        1.3 Sharing variables with ordinary functions or other class member functions

        1.4 Do declaration order for member variables

        1.5 imputation for member variables

2. Constructor and destructor

        2.1 Constructors and destructors are still actively created by default.

        2.2 Actively and explicitly define constructor and destructor requirements

        2.3 The destructor is released in reverse order of variable declaration

        2.4 It is recommended to use the initialization list to initialize member variables

        2.5 Initialize variables in declaration order

3. Copy construction and assignment operator

        3.1 Default copy construction and assignment operations

        3.2 Custom copy construction and assignment operations

       3.3 Prohibition of copy construction and assignment operations

        3.4 Ensure that the member variables used are assigned, and the assignment operation returns a left reference

Fourth, class member function behavior definition

        4.1 Do not rush to add behavior functions

        4.2 Planning for new behaviors

        4.3 Overloading member functions carefully

        4.4 Membership Conflicts under Prudent Inheritance System

Five, class membership constraints

        5.1 Function and const application

        5.2 inline specifier and member variable

        5.3 Description of static members - static

6. Source code supplement


1. Plan member variables

        1.1 Ensuring the encapsulation of member variables

        If it is not a project development in C language, try not to use struct to organize your classes, because this will make your member variables lose encapsulation. If you make a data member public, everyone can read and write to it.

        It is a good habit to place member variables in the private access authority and provide read-only, write-only, and read-write access to these member variables.

class Commit_Access {
public:
    int getreadonly() const{ return readonly; }
    void setreadwrite(int value) { readwrite = value; }
    int getreadwrite() const { return readwrite; }
    void setwriteonly(int value) { writeonly = value; }
private:
    int noaccess;   // 纯内部数据,禁止访问这个 int
    int readonly;   // 可以只读这个 int
    int readwrite;  // 可以读/写这个 int
    int writeonly;  // 可以只写这个 int
};

        1.2 Sharing variables with derived classes

        For member variables that need to be inherited by derived classes, place them in the protected access permission and only provide them for shared use by derived classes:

class Commit_Access {
public:
    int getderivedataonly(){return derivedata;}
protected:
    int derivedata; //子类可用
private:

};

class Derived : public Commit_Access
{
private:
    /* data */
    int indata;
public:
    void setpdataonly( int val ){derivedata=val;}   //
};

        1.3 Sharing variables with ordinary functions or other class member functions

        For ordinary functions or other class member functions that do need to use class internal member variables, declare them as friends. If other classes do not have most member functions that need to use the internal member variables of this class, it is recommended not to declare the class as a friend, but to declare other class member functions that need to use internal member variables as friends separately.

//test1.h
#include <ostream>
class PTest;
class FriendTest
{
private:
    /* data */
public:
    void doit();
    void dosomething(PTest* const obj);
};

class PTest
{
private:
    /* data */
    int val;
public:
    friend std::ostream& operator<<(std::ostream& os, const PTest& obj);
    friend void FriendTest::doit();    //针对成员函数友元而非类
    friend void FriendTest::dosomething(PTest* const obj);
};
//test1.cpp
std::ostream& operator<<(std::ostream& os, const PTest& obj)
{
    os << obj.val;
    return os;
}

void FriendTest::doit()
{
    PTest obj;
    ++obj.val;
};

void FriendTest::dosomething(PTest* const obj)
{
    (obj->val)+=10;
};

        1.4 Do declaration order for member variables

        If there are ordinary member variables and dynamic member variables (pointer variables, dynamic memory allocation) in the class, in the order of declaration, please put the ordinary member variable declarations in front, and put the dynamic member variables in the back, so as to facilitate the initialization list later. For ordinary member variables, static member variables and const member variables come first, followed by built-in type variables, and then custom class type member variables. The same is true for dynamic pointer variables, the built-in type pointer variable comes first, and the custom class type pointer variable follows.

class A
{
private:
    int val;
public:
};

class B{
    private:
    int val;
public:
};

class C
{
private:
    static int si;
    const char cc = 'D';
    int ival;
    double dval;
    A a;
    char *pc;
    B *pb;
public: 
};

        For member variables of the same level type, it is best to put the short-length class type first and the long-length class type last, which is beneficial both from the perspective of memory alignment and from the efficiency of construction and initialization:

class D
{
private:
    bool b1;
    char c1;
    short s1;
    int ival;
    double dval;
    int vec[5];
public:
};

/*不良习惯
class D
{
private:
    double dval;
    bool b1;
    int vec[5];
    char c1;
    int ival;
    short s1; 
public:
};
*/

        1.5 imputation for member variables

        For some closely related variables, in addition to placing them together for logical understanding, it is best to merge them into a sub-object to bring these variables and their behavior together. For example, in the following example, when a double-ended queue and a thread lock are used as member variables of the network reading information interface, they can be completely separated and used as a cache queue class, allowing the class to enqueue, dequeue, and capacity the data And other behaviors are responsible, the network read information interface class only uses the cache queue object:

#include <queue>
#include <mutex>

class CacheQue
{
private:
    std::deque<std::string>  msgque;
    std::mutex               msgmutex;
public:
    //func
};
class ReadFromNet
{
private:
    // std::deque<std::string>  msgque;
    // std::mutex               msgmutex;
    //other data
    int readflag;
    int ival;
    //...
    CacheQue msgs;
public:
    //func
};

2. Constructor and destructor

        2.1 Constructors and destructors are still actively created by default.

        Almost all classes have one or more constructors and a destructor, whether created explicitly or by default by the compiler. Because they provide the most basic functions. The constructor controls the basic operation of the object when it is generated and ensures that the object is initialized; the destructor destroys an object and ensures that it is completely cleared.

        Implicit constructor support is usually adopted for ordinary member variables that contain C++ standard built-in types, such as various atomic types, containers, and so on.

class Obj1
{
private:
    /* data */
    int id;
    double val;
    std::vector<int> vec;    //OK
    std::string str;         //OK
public:    
    //default func
};

        For example, the class type member variable is a dynamic handle, and the implicitly defined destructor will not correctly realize the idea of ​​the class type designer, which requires manual definition of the destructor. If a class requires a user-defined destructor, it also requires the user to define at least one constructor.

class Obj2
{
private:
    /* data */
    char* pstr;
public:
    Obj2(){pstr = (char*)malloc(10*sizeof(char));}//手动显式构造函数
    virtual ~Obj2(){delete[] pstr; pstr=nullptr;}         //手动显式析构函数
    //default func
};

        If there is a dynamic handle in the member variable, it is best to declare the destructor as a virtual virtual function , so that the derived class does not need to actively release the memory of the base class when it is inherited and used by the user.

        2.2 Actively and explicitly define constructor and destructor requirements

        In addition, when the custom class type contains the following member variables, it is also necessary to explicitly define the constructor and destructor:

  • type T has a non-static data member of a const-qualified non-class type (or array thereof);
  • Type T has non-static data members of reference type;
  • Type T has non-static data members that cannot be copy-assigned, a direct base class or a virtual base class;
  • A type T is a union-like class and has a variant member whose corresponding copy-assignment operator is nontrivial.
class Obj3{ private: int ival;};
class Base1{ 
public:
    Base1(){ ptr = new Obj3();};
    virtual ~Base1(){ if(nullptr!=ptr){delete ptr; ptr = nullptr;}};
private: 
    Obj3 *ptr; 
};
class Obj4 : public Base1
{
public:
    //...other
    Obj4(): Base1(),val(0){ ptr = new Obj3(); };          //构造函数
    ~Obj4(){if(nullptr!=ptr){delete ptr; ptr = nullptr;}};//析构函数
private:
    const int a = 10; //
    int val;
    Obj3 *ptr;        //
};

        In addition, if a certain type needs to be used as a base class, even if there is no member variable for dynamic memory allocation, it is best to declare the destructor as a virtual virtual function . For more design requirements of virtual functions and classes, please refer to the previous blog post of this column (Part 3).

        2.3 The destructor is released in reverse order of variable declaration

        When the destructor releases the dynamic member variable memory, it is best to release it in the reverse order of the declaration order of the member variables:

class Base1{ 
public:
    Base1(){ 
        ptr = new Obj3(); 
        pc = new char[10];
    };
    virtual ~Base1(){ 
        if(nullptr!=pc){delete[] pc; pc = nullptr;}
        if(nullptr!=ptr){delete ptr; ptr = nullptr;}
    };
private: 
    Obj3 *ptr; 
    char *pc;
};

        2.4 It is recommended to use the initialization list to initialize member variables

        Try to use initialization instead of assignment in the constructor:

        const and reference data members can only be initialized, not assigned.

        The pointer member object may cause pointer confusion or assignment prohibition when performing copy and assignment operations.

        Initializing member variables with a member initialization list is more efficient than assigning and initializing member variables in a constructor, because when using a member initialization list, only one member function is called. When assigning a value in the constructor, there will be two calls.

class Base2{ 
public:
    Base2(int val){ ptr = new int(val);};
    virtual ~Base2(){ if(nullptr!=ptr){delete ptr; ptr = nullptr;}};
private: 
    Base2(const Base2&) = delete;
    Base2& operator=(const Base2&) = delete;
    int *ptr; 
};

class Obj5
{
public:
    Obj5(const int &ival_, Base2* pc_) : icval(10),ival(ival_),pc(pc_){};
    virtual ~Obj5(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private:
    const int icval;//必须通过成员初始化列表进行初始化
    int ival;
    const Base2* pc;//必须通过成员初始化列表进行初始化
};

        Even for a simple type, unnecessary duplication of function calls can be very costly. Especially for custom types, as business extension classes become larger and more complex, their constructors also become larger and more complex, and the cost of object creation becomes higher and higher. Getting into the habit of using member initialization lists whenever possible not only satisfies the requirements for const and reference member initialization, but also greatly reduces the chances of inefficiently initializing data members. In short, initialization through the member initialization list is always legal, and the efficiency is by no means lower than assignment in the constructor body, it is only more efficient.

        2.5 Initialize variables in declaration order

        Class members are initialized in the order in which they are declared within the class, regardless of the order in which they are listed in the member initialization list. In addition, the base class data members are always initialized before the derived class data members, so when using inheritance, the initialization of the base class should be listed at the top of the member initialization list.

class Base2{ 
public:
    Base2(int val){ ptr = new int(val);};
    virtual ~Base2(){ if(nullptr!=ptr){delete ptr; ptr = nullptr;}};
private: 
    Base2(const Base2&) = delete;
    Base2& operator=(const Base2&) = delete;
    int *ptr; 
};
class Obj6 : public Base2
{
public: 
    Obj6(const bool& bflag_, const int& ival_, const double& dval_,const int& size=10)
        : Base2(1),bflag(bflag_), ival(ival_), dval(dval_)/*, pc(nullptr)*/
    {
        pc = (char*)malloc(size*sizeof(char));
    };
    virtual ~Obj6(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private: 
    bool bflag;
    int ival;
    double dval;
    char* pc;
};

class Obj7 : public Base2
{
public: 
    Obj7(const bool& bflag_, const int& ival_, const double& dval_,const int& size=10)
        : ival(ival_), Base2(1), dval(dval_),bflag(bflag_)/*, pc(nullptr)*/
    {
        pc = (char*)malloc(size*sizeof(char));
    };
    virtual ~Obj7(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private: 
    bool bflag;
    int ival;
    double dval;
    char* pc;
};
//
#include <iostream>
#include <chrono>
const unsigned long sizel = 10000;
void test1(void)
{
    auto start = std::chrono::system_clock::now();
    for (size_t row = 0; row < sizel; row++)
    for (size_t i = 0; i < sizel; i++)
    {
        Obj6 obj6(i/2,i%2,i*1.0);
    }
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double,std::milli> diff = end-start;
    std::cout << "Obj6 diff.count() = " <<  diff.count() << "ms\n";
    
    start = std::chrono::system_clock::now();
    for (size_t row = 0; row < sizel; row++)
    for (size_t i = 0; i < sizel; i++)
    {
        Obj7 obj7(i/2,i%2,i*1.0);
    }
    end = std::chrono::system_clock::now();
    diff = end-start;
    std::cout << "Obj7 diff.count() = " <<  diff.count() << "ms\n";
};
//
D:\workForMy\workspace\class_test4_c++>test.exe
Obj6 diff.count() = 13927.5ms
Obj7 diff.count() = 13970.9ms

        In the above code, Base2 will always be initialized first (of course, it can also be implicitly initialized), followed by bflag, iva, dvall and pc. For all members of an object, their destructors are always called in the reverse order in which they were created in the constructor. Then, if it is not initialized in the order of declaration, the compiler must track the order in which its members are initialized for each object to ensure that their destructors are called in the correct order. This comes with expensive overhead. Therefore, in order to avoid this overhead, the compiler will process the members in the same order in the process of creation (construction) and destruction (destruction) of all objects of the same type, regardless of the members in the initialization list. in what order.

        Look at the following example again, first initialize the size, and then use the size to initialize the vector, but the desired result is not obtained.

#include <vector>
#include <iostream>

class Obj8
{
public:   
    Obj8(int size_) :size(size_), ivec(size){};
    ~Obj8(){};
    void test(){
        std::cout << "size = "<<size<<"\n";
        std::cout << "ivec.size = "<<ivec.size()<<"\n";
    }
private: 
    std::vector<int> ivec;
    int size;
};
void test1(void)
{
    Obj8 obj8(10);
    obj8.test();
};
//out log
size = 10
ivec.size = 6290744    //为啥不是10呢

        This is because class members are initialized in the order in which they are declared in the class, which has nothing to do with the order in which they are listed in the member initialization list, that is, vector is initialized first, and an indeterminate value is passed in, so vector It didn't wait until size was initialized first, but it was initialized before size. The above code is correct only if the declaration order of the two members is swapped.

//交换声明次序,其他不变    
int size;
std::vector<int> ivec; 
//out log
size = 10
ivec.size = 10

3. Copy construction and assignment operator

        3.1 Default copy construction and assignment operations

        For non-dynamic variables whose member variables are built-in types, although the default copy constructor and copy assignment function can be used, it is recommended to use the default keyword to explicitly mark:

#include <string>
class Obj9
{
public:
    Obj9() = default;
    Obj9(const Obj9&) = default;
    Obj9& operator=(const Obj9&) = default;
private:    
    bool bval;
    int ival;
    std::string str;
};
//
    Obj9 Obj9_1;
    Obj9 Obj9_2(Obj9_1);
    Obj9 Obj9_3;
    Obj9_2 = Obj9_1;

        3.2 Custom copy construction and assignment operations

        For classes that need to dynamically allocate memory, explicitly declare a copy constructor and an assignment operator or explicitly prohibit copying and assignment .

        Because for a class that dynamically allocates memory, if the copy constructor and assignment operator are not clearly defined or prohibited, when using the call, for example, if the class has a char* variable, use operator=, C++ will generate and call a default operator= operator. The default assignment operator performs a member-by-member assignment from a's member to b's member, which is a bitwise copy for pointers (a.data and b.data). There is a possibility that the memory pointed to by b will never be deleted, so it will be lost forever, resulting in a memory leak. Or now the pointers contained in a and b point to the same string, so as long as one of them leaves its living space, its destructor will delete the piece of memory that the other pointer still points to. The second exception also occurs when using the copy constructor.

//test1.h
class MyString
{
public:
	MyString(void);							//默认构造函数
	MyString( const char *str = nullptr );	//普通构造函数
	MyString( const MyString &other );		//拷贝构造函数
	~MyString( void );						//析构函数
	MyString& operator=(const MyString &other);	//赋值函数
    MyString& operator=(const char* other);	    //赋值函数
	char* c_str(void) const;					//取值(取值)
private:
    void init(const char *str);
    void copy(const char *str);
private:
	char *m_data;
};
//test1.cpp
#include <cstring>
//默认构造函数
MyString::MyString(void)
{
	MyString(nullptr);	//内部调用普通构造函数
}
//普通构造函数
MyString::MyString(const char *str)
{
    init(str);
}
// MyString 的析构函数
MyString::~MyString(void)
{
	delete [] m_data; // 或 delete m_data;
}

void MyString::init(const char *str)
{
	if(nullptr==str)
	{
		m_data = new char[1];	//对空字符串自动申请存放结束标志'\0'的空
		*m_data = '\0';
	}else{
		int length = strlen(str);
		m_data = new char[length+1]; // 分配内存
		strcpy(m_data, str);
	}
};

void MyString::copy(const char *str)
{
    if(nullptr!=str)
    {
        delete [] m_data;				//释放原有的内存资源
        int length = strlen( str );
        m_data = new char[length+1];	//重新分配内存
        strcpy( m_data, str);
    }
};

//拷贝构造函数
MyString::MyString( const MyString &other ) //输入参数为const型
{
    init(other.m_data);
}

//赋值函数
MyString &MyString::operator =( const MyString &other )//输入参数为const型
{
	if(this == &other)				//检查自赋值
		return *this;	
    copy(other.m_data);	
	return *this;					//返回本对象的引用
}

MyString& MyString::operator=(const char* other)	    //赋值函数
{
    copy(other);	
    return *this;					//返回本对象的引用
};

char* MyString::c_str(void) const
{
	return (char*)m_data;
}
//
    MyString mstr1("hello");
    MyString mstr2("");
    mstr2 = mstr1;
    MyString mstr3 = mstr2;
    mstr3 = "world";

       3.3 Prohibition of copy construction and assignment operations

         If you don't need to disallow copy construction and copy assignment functions, explicitly disallow them , in the following way:

  • Declare the copy construction and copy assignment functions as private functions, which can only be used by functions that declare friends or internal member functions.
  • Declare the copy construction and copy assignment functions as private functions and define them empty. Although they can be called, they are invalid.
  • Declare the copy construction and copy assignment functions as private functions and use the keyword delete to explicitly delete, and any calls will alarm when compiling.
class Obj10
{
public:
    Obj10()= default;
private:
    //方式一,自定义提供拷贝构造及复制赋值
    // Obj10(const Obj10&){/*code*/};
    // Obj10& operator=(const Obj10&){/*code*/return *this;};
    //方式二,提供默认拷贝构造及复制赋值
    // Obj10(const Obj10&) =default;
    // Obj10& operator=(const Obj10&) = default;
    //方式三,自定义提供空的拷贝构造及复制赋值
    // Obj10(const Obj10&){/*不做任何处理*/};
    // Obj10& operator=(const Obj10&){/*不做任何处理*/return *this;};
    //方式四,强制删除拷贝构造及复制赋值,禁止任何函数调用
    Obj10(const Obj10&) = delete;
    Obj10& operator=(const Obj10&) = delete;
private:    
    bool bval;
    int ival;
    std::string str;
    char* pc;
};

        3.4 Ensure that the member variables used are assigned, and the assignment operation returns a left reference

        Because the associativity of the assignment operator is from right to left, the return value of operator= must be accepted by the function itself as an input parameter, following the principle that the input and return of operator= are all references to class objects. When defining your own assignment operator, you must return the reference "*this" of the left parameter of the assignment operator. Failure to do so will result in continuous assignment not being possible, or implicit type conversion at call time, or both.

        At the same time, it should be noted that when defining the copy constructor and assignment operator, ensure that all data members are assigned, especially when adding new data members to the class, remember to update the copy constructor and assignment operator function.

        When the user calls the type, it realizes the situation of assigning itself (obj1=obj1, obj1=obj2 but obj2 is an alias of obj1), so it is necessary to ensure that the assignment to itself is checked in the operator= function. Because an assignment operator must first release an object's resources (remove the old value), and then allocate new resources based on the new value. In the case of self-assignment, it would be disastrous to free the old resource, because the old resource would be needed when the new resource was allocated.

class Obj11
{
public:
    Obj11(const int& ival_, const double& dval_=0.0) 
    : ival(ival_),dval(dval_){ };
    Obj11(const Obj11& obj){
        ival = obj.ival;
        dval = obj.dval;
    };
    Obj11& operator=(const Obj11& obj){
        if(this==&obj) return *this;
        ival = obj.ival;
        dval = obj.dval;
        return *this;
    };
private:    
    int ival;
    double dval;
};
//
    Obj11 Obj11_1(1), Obj11_2(2), Obj11_3(3), Obj11_4(4);
    Obj11_1 = Obj11_2 = Obj11_3 = Obj11_4;
    (Obj11_1 = Obj11_2) = Obj11_3 = Obj11_4;
    Obj11_1 = (Obj11_2 = Obj11_3) = Obj11_4;

        Define the operator= of the Obj11 type to return a reference to "*this", which can implement continuous (chain) assignment operations similar to built-in types.

Fourth, class member function behavior definition

        4.1 Do not rush to add behavior functions

        Usually, the member functions of a class are divided into the application scenarios, which are the basic member functions of the class and the member functions of the class behavior.

        Class basic member functions are the most basic constructors, destructors, copy constructors, and copy assignment functions discussed above; class behavior member functions are mainly based on the unique behaviors provided by type member variables for business applications, such as various operations Operator functions such as move copy construction, move assignment, class call function, general assignment operators, arithmetic operators, IO operators, etc., as well as the realization of complex behaviors such as search, sorting, character operations, and primitive processing.

        A class's initial design of member functions should include default implicit or explicit definition of the constructor (one or more), a destructor, explicit use or prohibition of the copy constructor, and the copy assignment operator.

        For example, the MyString class defined above implements two constructors, a destructor, a copy constructor, and two assignment operator functions. It provides built-in function functions to assist in the construction and assignment operation functions, and finally provides an acquisition function. The value function of the private variable:

class MyString
{
public:
	MyString(void);							//无参数构造函数,通过调用普通构造函数来实现的
	MyString( const char *str = nullptr );	//普通构造函数,通过调用了内置init函数实现
	MyString( const MyString &other );		//拷贝构造函数,也调用了内置init函数实现
	~MyString( void );						//一个析构函数
	MyString& operator=(const MyString &other);	//拷贝赋值函数,MyString到MyString,调用了内置copy函数实现
    MyString& operator=(const char* other);	    //转换赋值函数,char*到MyString,调用了内置copy函数实现
	char* c_str(void) const;					//取值(取值)
private:
    void init(const char *str);
    void copy(const char *str);
private:
	char *m_data;
};

        In the above code, the MyString type interface supports users to create objects with empty parameters or character pointers, delete objects, copy objects, transform character pointers into objects, and obtain character variable pointers to use storage content.

        The design of the class should follow the gradual addition of behavioral member functions as needed, provide an interface that satisfies any reasonable thing that the current scene business wants to do, and insist on an interface with as few functions as possible, and no two functions overlap each other.

        When adding a new function to an interface, consider carefully: Adding a new function on the premise that the interface is complete, whether the convenience it brings exceeds the additional cost it brings, such as complexity, readability, and maintainability sex and compile time etc. For example, a common piece of functionality would be more efficiently implemented as a member function, which would be a good reason to add it to an interface. Including adding a member function can prevent user errors and is also a strong basis for adding it to the interface.

        4.2 Planning for new behaviors

        A new added behavior is that it needs to be clarified whether it is provided as a member function, a normal function, or a friend function. If it is a member function, what kind of access rights needs to be provided, and whether it needs to be declared as a virtual function.

        It is a good habit to derive the positioning of new behaviors in the way of application requirements scenarios. For example, take MyString as an example. If you need to support output to ostream for display, if you position "operator<<" as a member function, although the effect of output to ostream can also be achieved, the usage habits are inconsistent with the standard library: One is to output the content "hello" to the ostream, and the other is to output the content "hello" to the ostream.

class MyString
{
public:
	//other
    std::ostream& operator<<(std::ostream& output){
        output << std::string(m_data);
        return output;
    };
    //other
private:
	char *m_data;
};
//
    MyString mstr1("hello");
    mstr1 << std::cout; //OK
    // std::cout << mstr1; //error
//out log
hello

        However, if operator<< is positioned as a normal function and supported by friends, then its output habits will be consistent with the standard library, and it will be more practical to output the content "hello" from ostream in terms of behavior.

class MyString
{
public:
    //other
    friend std::ostream& operator<<(std::ostream& output,const MyString& obj){
        output << std::string(obj.m_data);
        return output;
    };
    //other
private:
	char *m_data;
};
//
    MyString mstr1("hello");
    //mstr1 << std::cout; //
    std::cout << mstr1; //OK
//out log 
hello

        4.3 Overloading member functions carefully

        When the member functions have the same name, the same number of formal parameters, and almost the same implementation content, please consider whether the default actual parameters and type conversion can meet the requirements to save lengthy overloading design.

class Obj12
{
public:
    void func(){ std::cout<<"func()\n"; };
    void func(const char& sval_){std::cout<<"func(const char&)\n"; };
    void func(const int& ival_){std::cout<<"func(const int&)\n"; };
    void g(const int& ival_=0){std::cout<<"g(const int&)\n";};
private:
    int ival;
};
//
    Obj12 obj12;
    obj12.func();
    obj12.func('a');
    obj12.func(0);
    obj12.func(10);
    obj12.g();
    obj12.g('a');
    obj12.g(0);
    obj12.g(10);  
//out log
func()
func(const char&)
func(const int&)
func(const int&)
g(const int&)
g(const int&)
g(const int&)
g(const int&)

        4.4 Membership Conflicts under Prudent Inheritance System

        In the multiple inheritance system, especially the diamond inheritance system, there are often member conflicts, and it is necessary to carefully design the inheritance method and member definition of the class. Special consideration needs to be given to the combination of multiple inheritance and virtual functions. For this aspect, please refer to the third blog post "Classes and Virtual Functions" of this topic in this column. There are too many elaborations here.

class Base3 {
public:
    int doIt(){return 3;};// 一个叫做int doIt 的函数
};
class Base4
{
public:
    void doIt(){};// 一个叫做void doIt 的函数
};
class Derived1: public Base3, public Base4 { public: };

class Derived2: public Base3{};
class Derived3: public Base3{};
class Derived4: public Derived2,public Derived3{};
//
    Derived1 d1;
    // d1.doIt();         // 错误!——二义
    // int i1 = d1.doIt();// 错误!——二义
    Derived4 d4;
    // d4.doIt();            // 错误!——二义

        Many times, many classes are designed, and the program can be used when there is no ambiguity in use. This potential ambiguity cannot be discovered in time, it can be latent in the program for a long time, undetected and inactive, until a certain critical moment comes.

Five, class membership constraints

        5.1 Function and const application

        The const keyword is used extensively in classes. Inside a class, it can be used for both static and non-static members. For pointers, the pointer itself can be specified as const, the data pointed to by the pointer can also be specified as const, or both can be specified as const at the same time. In a function declaration, const can refer to the return value of the function, or a parameter; for member functions, it can also refer to the entire function.

        We usually use const plus "pass by reference" instead of "pass by value" to define member function parameters. Passing an object by value eventually leads to calling an object type copy constructor, while passing parameters by reference avoids constructing a new copy of the parameter and saves overhead. In addition, when an object of a derived class is passed by value as a base class object, its (derived class object) behavior characteristics as a derived class will be "cut" off, thus becoming a simple base class object, Use pass by reference to avoid cutting problems.

class X
{
public:
    int getVal(void) const{return ival;}
    void setVal(const int& ival_){
        ival=ival_;
        // ival_ += 1; //error
    };
    friend const X operator+(const X& lhs,const X& rhs){
        X ret(lhs);
        ret.ival+=rhs.ival;
        return ret;
    };
    friend X operator-(const X& lhs,const X& rhs){
        X ret(lhs);
        ret.ival-=rhs.ival;
        return ret;
    };
private:
    int ival;
};
//
    X x1,x2;
    x1.setVal(10);
    x2.setVal(20);
    (x1-x2)=x1; //OK
    //(x1+x2)=x1; //error
    x1=x1+x2;   //OK 

        Normally the return of a member function using const constraints cannot be modified. Any function that does not modify data members should be declared const. If you accidentally modify the data members or call other non-const member functions when writing const member functions, the compiler will point out the error, which will undoubtedly improve the robustness of the program. const For the const declaration of a member function, the const keyword can only be placed at the end of the function declaration. For ordinary functions and friend functions, the const keyword is placed at the front of the function declaration.

        In a const member function, the type of this is a const pointer to an object of const class type. Neither the object pointed to by this nor the address saved by this can be changed. If you add const decoration to the function return value in the "pointer passing" mode, then the content of the function return value (ie pointer) cannot be modified, and the return value can only be assigned to the same type of pointer with const decoration. Putting const after the function mainly restricts the member functions in the class, and placing const before the function restricts the modification of the return value through the pointer when the return type of the function is a pointer.

        With const, you can inform the compiler and other programmers that a value is to remain unchanged. Whenever this is the case, you need to use const explicitly, because doing so allows the compiler to help ensure that the constraint is not violated. However, sometimes the application of const constraints to return is also miscalculated. Some member functions that do not obey the const constraint definition can also pass the compilation test. For example, in the following example, an execution statement that "modifies the data pointed to by the pointer (member function operator char*() const)" obviously violates the const constraint (pchar1[0] = 'a';), but when compiling will pass.

class MyString
{
public:
    //other
    operator char*() const;                     //MyString转char*
	char* c_str(void) const;					//取值(取值)
    //other
private:
	char *m_data;
};
//test1.cpp
MyString::operator char*() const
{
    return (char*)m_data;
};

char* MyString::c_str(void) const
{
	return (char*)m_data;
};
//
    MyString mstr1("hello\n");
    std::cout << mstr1;    //OK
    char *pchar1 = mstr1;  //operator char*() const
    pchar1[0] = 'a';
    pchar1 = nullptr;
    std::cout << mstr1;   //OK,输出aello
    char *pchar2 = mstr1.c_str(); //
    // pchar2[0] = "b";   //error,不能将 "const char *" 类型的值分配到 "char" 类型的实体
    std::cout << mstr1;   //OK

        Of course, don't give up returning objects because of some advantages of functions returning references, and don't try to return a reference when you must return an object. Usually, for member functions, most of them are implemented by returning references, while for friend functions, most of them are implemented by returning objects.

Obj& Obj::operator-=(const Obj& rhs)
{
    ival -= rhs.ival;
    return *this;
}

Obj operator-(const Obj& lhs)
{
    Objret(lhs);
    ret.ival = -ret.ival;
    return ret;
}

        5.2 inlineSpecifiers and member variables

        The purpose of the inline specifier is to prompt the compiler to do optimizations, such as function inlining, which usually require the compiler to see the definition of the function. Compilers can (and often do) ignore the presence or absence of the inline specifier for optimization purposes. If the compiler inlines a function, it replaces all calls to it with the function body to avoid the overhead of function calls (putting data on the stack and getting results), which may result in a larger executable, Because the function may be repeated multiple times. The result is the same as a function-like macro, except that the identifier and macro used for the function refer to the definition seen at the point of definition, not to the definition at the point of call.

*inline 函数说明符,在用于函数的声明说明符序列时,将函数声明为一个内联(inline)函数。

1)完全在 class/struct/union 的定义之内定义,且被附着到全局模块 (C++20 起)的函数是隐式的
内联函数,无论它是成员函数还是非成员 friend 函数。

2)(C++11 起)声明有 constexpr 的函数是隐式的内联函数。弃置的函数是隐式的内联函数:其(弃置)
定义可出现在多于一个翻译单元中。
  
3)(C++17 起)inline 说明符,在用于具有静态存储期的变量(静态类成员或命名空间作用域变量)的
声明说明符序列时,将变量声明为内联变量。声明为 constexpr 的静态成员变量(但不是命名空间
作用域变量)是隐式的内联变量。

在内联函数中,
*所有函数定义中的函数局部静态对象在所有翻译单元间共享。
*所有函数定义中所定义的类型同样在所有翻译单元中相同。

 (C++17 起) 关键词 inline 对于函数的含义已经变为“容许多次定义”而不是“优先内联”,
因此这个含义也扩展到了变量。

        In C++, if a function is declared inline, it must be declared inline in every translation unit, and every inline function must have exactly the same definition). C++, on the other hand, allows non-const function-local static objects, and all function-local static objects from different definitions of an inline function are the same. Implicitly generated member functions, and any member functions declared as preset in their first declaration, are inlined like any other function defined inside a class definition.

class InlineTest
{
public:
    void inl(int& ival_){};          //inline隐式
    inline void f(int& ival_){};     //inline显式
    void g(char& cval_);
    constexpr void func(double& dval);//inline隐式,(C++11 起)
private:
    inline static int n = 1;    //inline变量
};
//test1.cpp
inline void InlineTest::g(char& cval_){};       //inline显式
constexpr void InlineTest::func(double& dval){};//inline隐式

        Regardless of inlining or not, inline functions guarantee the following semantics: any function with internal linkage can be declared static inline, with no other restrictions. A non-static inline function cannot define a non-const function-local static object, and cannot use file-scope static objects.

        5.3 静态成员Description - static

        Static members of a class are not associated with objects of the class: they are independent variables with static or thread (since C++11) storage duration, or regular functions. The static keyword will only be used in the declaration of a static member in the class definition, not in the definition of the static member.

class X { private:static int n; }; // 声明(用 'static')
int X::n = 1;              // 定义(不用 'static')

        Static member function:

  • Static member functions are not associated with any object. When invoked, they do not have a this pointer.
  • Static member functions cannot be virtual, const, or volatile.
  • The address of a static member function can be stored in a regular function pointer, but not in a member function pointer.

        Static data members:

  • Static data members are not associated with any object. They exist even if no object of the class is defined. There is only one instance of a static data member with static storage duration in the entire program, unless the keyword thread_local is used, in which case each thread has one instance of this object with thread storage duration (since C++11).
  • Static data members cannot be mutable.
  • At namespace scope, if the class itself has external linkage (that is, is not a member of an unnamed namespace), then the class's static data members also have external linkage. Local classes (classes defined inside functions) and unnamed classes, including member classes of unnamed classes, cannot have static data members.
  • Static data members can be declared inline. Inline static data members can be defined within a class definition and initializers can be assigned.
  • If a static data member of integral or enumeration type is declared const (and not volatile), it can be initialized directly within the class definition with an initializer in which each expression is a constant expression
  • If a static data member of a literal type (LiteralType) is declared constexpr, it must be initialized directly within the class definition with an initializer in which each expression is a constant expression
  • If a const non-inline (since C++17) static data member or a constexpr static data member (since C++11) (before C++17) is used ODR-wise, then the namespace-scoped definition is still required, but it There cannot be an initializer. A definition can be provided, although this is redundant (since C++17).
  • If a static data member is declared constexpr, it is implicitly inline and need not be redeclared at namespace scope. This redeclaration without an initializer (required before, as shown above) is still allowed, but has been deprecated (since C++17).
class Obj13
{
public:
    static void f(int ival_);
private:
    inline static int i = 1;
    const static int n = 1;
    const static int m{2}; // C++11 起
    const static int k;
    constexpr static int arr[] = { 1, 2, 3 };        // OK C++11 起
    // constexpr static int l; // 错误:constexpr static 要求初始化器
    constexpr static int l{1}; // OK C++11 起
};
//test1.cpp
void Obj13::f(int ival_){};
const int Obj13::k = 3;

6. Source code supplement

        Compile instruction g++ main.cpp test*.cpp -o test.exe -std=c++17 (inline acts on member variables)

        main.cpp

#include "test1.h"

int main(int argc, char* argv[])
{
    test1();
    return 0;
}

        test1.h

#ifndef _TEST_1_H_
#define _TEST_1_H_

class Commit_Access {
public:
    int getreadonly() const{ return readonly; }
    void setreadwrite(int value) { readwrite = value; }
    int getreadwrite() const { return readwrite; }
    void setwriteonly(int value) { writeonly = value; }
    int getderivedataonly(){return derivedata;}
protected:
    int derivedata; //子类可用
private:
    int noaccess;   // 纯内部数据,禁止访问这个 int
    int readonly;   // 可以只读这个 int
    int readwrite;  // 可以读/写这个 int
    int writeonly;  // 可以只写这个 int
};

class Derived : public Commit_Access
{
private:
    /* data */
    int indata;
public:
    void setpdataonly( int val ){derivedata=val;}   //
};

#include <ostream>
class PTest;
class FriendTest
{
private:
    /* data */
public:
    void doit();
    void dosomething(PTest* const obj);
};

class PTest
{
private:
    /* data */
    int val;
public:
    friend std::ostream& operator<<(std::ostream& os, const PTest& obj);
    friend void FriendTest::doit();
    friend void FriendTest::dosomething(PTest* const obj);
};

class A
{
private:
    int val;
public:
};

class B{
    private:
    int val;
public:
};

class C
{
private:
    static int si;
    const char cc = 'D';
    int ival;
    double dval;
    A a;
    char *pc;
    B *pb;
public: 
};

class D
{
private:
    bool b1;
    char c1;
    short s1;
    int ival;
    double dval;
    int vec[5];
public:
};

/*
class D
{
private:
    double dval;
    bool b1;
    int vec[5];
    char c1;
    int ival;
    short s1; 
public:
};
*/
#include <queue>
#include <mutex>

class CacheQue
{
private:
    std::deque<std::string>  msgque;
    std::mutex               msgmutex;
public:
    //func
};
class ReadFromNet
{
private:
    // std::deque<std::string>  msgque;
    // std::mutex               msgmutex;
    //other data
    int readflag;
    int ival;
    //...
    CacheQue msgs;
public:
    //func
};

class Obj1
{
private:
    /* data */
    int id;
    double val;
    std::vector<int> vec;    //OK
    std::string str;         //OK
public:    
    //default func
};

class Obj2
{
private:
    /* data */
    char* pstr;
public:
    Obj2(){pstr = (char*)malloc(10*sizeof(char));}//手动显式构造函数
    virtual ~Obj2(){delete[] pstr; pstr=nullptr;}         //手动显式析构函数
    //default func
};

class Obj3{ private: int ival;};
class Base1{ 
public:
    Base1(){ 
        ptr = new Obj3(); 
        pc = new char[10];
    };
    virtual ~Base1(){ 
        if(nullptr!=pc){delete[] pc; pc = nullptr;}
        if(nullptr!=ptr){delete ptr; ptr = nullptr;}
    };
private: 
    Obj3 *ptr; 
    char *pc;
};
class Obj4 : public Base1
{
public:
    //...other
    Obj4(): Base1(),val(0){ ptr = new Obj3(); }; //构造函数
    ~Obj4(){if(nullptr!=ptr){delete ptr; ptr = nullptr;}};// 析构函数
private:
    const int a = 10; //
    int val;
    Obj3 *ptr;        //
};

class Base2{ 
public:
    Base2(int val){ ptr = new int(val);};
    virtual ~Base2(){ if(nullptr!=ptr){delete ptr; ptr = nullptr;}};
private: 
    Base2(const Base2&) = delete;
    Base2& operator=(const Base2&) = delete;
    int *ptr; 
};

class Obj5
{
public:
    Obj5(const int &ival_, Base2* pc_) : icval(10),ival(ival_),pc(pc_){};
    virtual ~Obj5(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private:
    const int icval;//必须通过成员初始化列表进行初始化
    int ival;
    const Base2* pc;//必须通过成员初始化列表进行初始化
};

class Obj6 : public Base2
{
public: 
    Obj6(const bool& bflag_, const int& ival_, const double& dval_,const int& size=10)
        : Base2(1),bflag(bflag_), ival(ival_), dval(dval_)/*, pc(nullptr)*/
    {
        pc = (char*)malloc(size*sizeof(char));
    };
    virtual ~Obj6(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private: 
    bool bflag;
    int ival;
    double dval;
    char* pc;
};

class Obj7 : public Base2
{
public: 
    Obj7(const bool& bflag_, const int& ival_, const double& dval_,const int& size=10)
        : ival(ival_), Base2(1), dval(dval_),bflag(bflag_)/*, pc(nullptr)*/
    {
        pc = (char*)malloc(size*sizeof(char));
    };
    virtual ~Obj7(){ if(nullptr!=pc){delete pc; pc=nullptr;}};
private: 
    bool bflag;
    int ival;
    double dval;
    char* pc;
};

#include <vector>
#include <iostream>

class Obj8
{
public:   
    Obj8(int size_) :size(size_), ivec(size){};
    ~Obj8(){};
    void test(){
        std::cout << "size = "<<size<<"\n";
        std::cout << "ivec.size = "<<ivec.size()<<"\n";
    }
private: 
    int size;
    std::vector<int> ivec; 
};

#include <string>
class Obj9
{
public:
    Obj9()= default;
    Obj9(const Obj9&) = default;
    Obj9& operator=(const Obj9&) = default;
private:    
    bool bval;
    int ival;
    std::string str;
};

// #include <ostream>
class MyString
{
public:
	MyString(void);							//默认构造函数
	MyString( const char *str = nullptr );	//普通构造函数
	MyString( const MyString &other );		//拷贝构造函数
	~MyString( void );						//析构函数
	MyString& operator=(const MyString &other);	//赋值函数
    MyString& operator=(const char* other);	    //赋值函数
    operator char*() const;                     //MyString转char*
	char* c_str(void) const;					//取值(取值)

    std::ostream& operator<<(std::ostream& output){
        output << std::string(m_data);
        return output;
    };
    friend std::ostream& operator<<(std::ostream& output,const MyString& obj){
        output << std::string(obj.m_data);
        return output;
    };
private:
    void init(const char *str);
    void copy(const char *str);
private:
	char *m_data;
};

class Obj10
{
public:
    Obj10()= default;
private:
    //方式一,自定义提供拷贝构造及复制赋值
    // Obj10(const Obj10&){/*code*/};
    // Obj10& operator=(const Obj10&){/*code*/return *this;};
    //方式二,提供默认拷贝构造及复制赋值
    // Obj10(const Obj10&) =default;
    // Obj10& operator=(const Obj10&) = default;
    //方式三,自定义提供空的拷贝构造及复制赋值
    // Obj10(const Obj10&){/*不做任何处理*/};
    // Obj10& operator=(const Obj10&){/*不做任何处理*/return *this;};
    //方式四,强制删除拷贝构造及复制赋值,禁止任何函数调用
    Obj10(const Obj10&) = delete;
    Obj10& operator=(const Obj10&) = delete;
private:    
    bool bval;
    int ival;
    std::string str;
    char* pc;
};

class Obj11
{
public:
    Obj11(const int& ival_, const double& dval_=0.0) 
    : ival(ival_),dval(dval_){ };
    Obj11(const Obj11& obj){
        ival = obj.ival;
        dval = obj.dval;
    };
    Obj11& operator=(const Obj11& obj){
        if(this==&obj) return *this;
        ival = obj.ival;
        dval = obj.dval;
        return *this;
    };
private:    
    int ival;
    double dval;
};

class X
{
public:
    int getVal(void) const{return ival;}
    void setVal(const int& ival_){
        ival=ival_;
        // ival_ += 1; //error
    };
    friend const X operator+(const X& lhs,const X& rhs){
        X ret(lhs);
        ret.ival+=rhs.ival;
        return ret;
    };
    friend X operator-(const X& lhs,const X& rhs){
        X ret(lhs);
        ret.ival-=rhs.ival;
        return ret;
    };
private:
    int ival;
};

class Obj12
{
public:
    void func(){ std::cout<<"func()\n"; };
    void func(const char& sval_){std::cout<<"func(const char&)\n"; };
    void func(const int& ival_){std::cout<<"func(const int&)\n"; };
    void g(const int& ival_=0){std::cout<<"g(const int&)\n";};
private:
    int ival;
};

class Base3 {
public:
    int doIt(){return 3;};// 一个叫做int doIt 的函数
};
class Base4
{
public:
    void doIt(){};// 一个叫做void doIt 的函数
};
class Derived1: public Base3, public Base4 { public: };

class Derived2: public Base3{};
class Derived3: public Base3{};
class Derived4: public Derived2,public Derived3{};

class InlineTest
{
public:
    void inl(int& ival_){};          //inline隐式
    inline void f(int& ival_){};     //inline显式
    void g(char& cval_);
    constexpr void func(double& dval);//inline隐式,(C++11 起)
private:
    inline static int n = 1;    //inline变量
};

class Obj13
{
public:
    static void f(int ival_);
private:
    inline static int i = 1;
    const static int n = 1;
    const static int m{2}; // C++11 起
    const static int k;
    constexpr static int arr[] = { 1, 2, 3 };        // OK C++11 起
    // constexpr static int l; // 错误:constexpr static 要求初始化器
    constexpr static int l{1}; // OK C++11 起
};

void test1(void);

#endif //_TEST_1_H_

        test1.cpp

#include "test1.h"

std::ostream& operator<<(std::ostream& os, const PTest& obj)
{
    os << obj.val;
    return os;
}

void FriendTest::doit()
{
    PTest obj;
    ++obj.val;
};

void FriendTest::dosomething(PTest* const obj)
{
    (obj->val)+=10;
};

#include <cstring>
//默认构造函数
MyString::MyString(void)
{
	MyString(nullptr);	//内部调用普通构造函数
}
//普通构造函数
MyString::MyString(const char *str)
{
    init(str);
}
// MyString 的析构函数
MyString::~MyString(void)
{
	delete [] m_data; // 或 delete m_data;
}

void MyString::init(const char *str)
{
	if(nullptr==str)
	{
		m_data = new char[1];	//对空字符串自动申请存放结束标志'\0'的空
		*m_data = '\0';
	}else{
		int length = strlen(str);
		m_data = new char[length+1]; // 分配内存
		strcpy(m_data, str);
	}
};

void MyString::copy(const char *str)
{
    if(nullptr!=str)
    {
        delete [] m_data;				//释放原有的内存资源
        int length = strlen( str );
        m_data = new char[length+1];	//重新分配内存
        strcpy( m_data, str);
    }
};

//拷贝构造函数
MyString::MyString( const MyString &other ) //输入参数为const型
{
    init(other.m_data);
}

//赋值函数
MyString &MyString::operator =( const MyString &other )//输入参数为const型
{
	if(this == &other)				//检查自赋值
		return *this;	
    copy(other.m_data);	
	return *this;					//返回本对象的引用
}

MyString& MyString::operator=(const char* other)	    //赋值函数
{
    copy(other);	
    return *this;					//返回本对象的引用
};

MyString::operator char*() const
{
    return (char*)m_data;
};

char* MyString::c_str(void) const
{
	return (char*)m_data;
};

#include <iostream>
#include <chrono>
const unsigned long sizel = 10000;
void initialization_test(void)
{
   auto start = std::chrono::system_clock::now();
    for (size_t row = 0; row < sizel/10; row++)
    for (size_t i = 0; i < sizel; i++)
    {
        Obj6 obj6(i/2,i%2,i*1.0);
    }
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double,std::milli> diff = end-start;
    std::cout << "Obj6 diff.count() = " <<  diff.count() << "ms\n";
    
    start = std::chrono::system_clock::now();
    for (size_t row = 0; row < sizel/10; row++)
    for (size_t i = 0; i < sizel; i++)
    {
        Obj7 obj7(i/2,i%2,i*1.0);
    }
    end = std::chrono::system_clock::now();
    diff = end-start;
    std::cout << "Obj7 diff.count() = " <<  diff.count() << "ms\n";
}
//
inline void InlineTest::g(char& cval_){};       //inline显式
constexpr void InlineTest::func(double& dval){};//inline隐式
//
void Obj13::f(int ival_){};
const int Obj13::k = 3;

void test1(void)
{
    Obj5 obj5(1,new Base2(1));
    //
    // initialization_test();
    //
    Obj8 obj8(10);
    obj8.test();
    //
    Obj9 Obj9_1;
    Obj9 Obj9_2(Obj9_1);
    Obj9 Obj9_3;
    Obj9_2 = Obj9_1;
    //
    MyString mstr1("hello\n");
    mstr1 << std::cout; //OK,不建议
    std::cout << mstr1; //OK
    char *pchar1 = mstr1;       //operator char*() const
    pchar1[0] = 'a';
    pchar1 = nullptr;
    std::cout << mstr1; //OK
    char *pchar2 = mstr1.c_str();
    // pchar2[0] = "b";    //error,不能将 "const char *" 类型的值分配到 "char" 类型的实体
    std::cout << mstr1; //OK
    MyString mstr2("");
    mstr2 = mstr1;
    MyString mstr3 = mstr2;
    mstr3 = "world";
    //
    Obj11 Obj11_1(1), Obj11_2(2), Obj11_3(3), Obj11_4(4);
    Obj11_1 = Obj11_2 = Obj11_3 = Obj11_4;
    (Obj11_1 = Obj11_2) = Obj11_3 = Obj11_4;
    Obj11_1 = (Obj11_2 = Obj11_3) = Obj11_4;
    //
    X x1,x2;
    x1.setVal(10);
    x2.setVal(20);
    (x1-x2)=x1; //OK
    // (x1+x2)=x1; //error
    x1=x1+x2;   //OK 
    //
    Obj12 obj12;
    obj12.func();
    obj12.func('a');
    obj12.func(0);
    obj12.func(10);
    obj12.g();
    obj12.g('a');
    obj12.g(0);
    obj12.g(10); 
    //
    Derived1 d1;
    // d1.doIt();         // 错误!——二义
    // int i1 = d1.doIt();// 错误!——二义
    Derived4 d4;
    // d4.doIt();            // 错误!——二义
};

Guess you like

Origin blog.csdn.net/py8105/article/details/129634949