C++类的介绍

2023-11-11

最近在学习SLAM,顺便将C++类的知识复习一下。(其中部分官方定义和程序设计方法来源于西北工业大学魏英老师)

 

1.类的定义:

是用户自定义的数据类型。
C++一个类定义的形式如下:
class 类名 
{
        成员列表
};
成员列表是类成员的集合,数目可以任意多, 一对 { } 是成员列表边界符,与成员列表一起成为类体。类体后面必须用 ; 结束。
1.每个类可以没有成员,也可以有多个成员。
2.类成员可以是数据或函数。
3.所有成员必须在类内部声明,一旦类定义完成后,就没有任何其他方式可以再增加或减少成员。
 
在面向对象程序设计中,一般将变量(数据)隐蔽起来,外部不能直接访问。把成员函数作为对外界的接口,通过成员函数访问数据,可能一开始学习的时候不太理解,这个我们在后面会经常用到,请耐心观看。
 
类中如果有成员函数,则声明是必须的,而定义是可选的,什么意思呢,请看下例:
 
在类内部定义函数体
class 类名
{
         返回类型   函数名(形参列表)
         {
                  函数体
         }
};
 
在类外部定义函数体
class 类名
{
         返回类型   函数名(形参列表);
};
返回类型   类名 :: 函数名(形参列表)
         函数体
 
看到这里会产生一个问题,那就是这两种定义方法到底有什么区别,或者根本没有区别。
其实它们还是有区别的,类内部定义的函数,程序在要调用它的时候会把它当作是一个内联函数,内联函数的好处是调用速度更快,但是会占用额外的内存空间,每调用一次都相当于定义一次。而外部定义的函数,就不会被当作内联函数。对于一些要用到递归的函数,定义成内联函数肯定是不合理的。因此建议使用第二种方法定义成员函数。
 
类的定义一般放在程序文件开头,或者放到头文件中被程序文件包含,当然也可以放在局部作用域里。这里有必要提一下,c++规定,在局部作用域中声明的类,成员函数必须是函数定义形式,不能是原型声明。
 
类相当于一种新的数据类型,数据类型不占用存储空间,用类型定义一个实体的时候,才会为它分配存储空间。
 
 

2.类成员的访问控制:

对类的成员进行访问,有两个访问源:类成员和类用户。
类成员指类本身的成员函数,类用户指类外部的使用者,包括全局函数,另一个类的成员函数等。
在C++中,类的每个成员都有访问控制属性:public(公有的)、private(私有的)、protected(保护的)
类用户想要访问类的数据成员,必须通过公有成员访问。
上面说过,面向对象程序设计过程中一般将数据隐蔽起来,也就是说一般的变量(数据)都声明为private,而成员函数声明为public,protected在后面我们会用到,不考虑继承的话,和private的性质一致。如果在声明的时候不写访问控制属性,则类会默认它为private。
 
在刚才类定义的基础上进行扩展:
class 类名
{
public:
     公有的数据成员和成员函数
protected:
     保护的数据成员和成员函数
private:
     私有的数据成员和成员函数
};
 
类的成员函数和普通函数一样,也可以进行重载,设置默认参数,显式的指定为内联函数等。
这里有个小问题,请看下例:
class Test
{
public:
    void Sum(int a=0,int b=0);
};
void Test::Sum(int a=0,int b=0)
{
    cout<<a+b;
}
这是一个设置了默认参数的函数,但是很遗憾,这是错误的,下面这样则是正确的:
class Test
{
public:
    void Sum(int a=0,int b=0);
};
void Test::Sum(int a,int b)
{
    cout<<a+b;
}
原因是C++中对于特定的某个函数,设置默认形参这个动作只能有一次
 
 

3.对象的定义和使用:

说了这么多,怎么样才能实现在外部实现对类成员的访问呢?这就是我们要讨论的对象。
 
对类的定义就是定义了一个具体的数据类型,要使用它必须将类实例化,即定义该类的对象。
以下两种定义类对象的方法都是合法的(假定有一个Test类):
Test test1 , test2;
class Test test1 , test2;
之前说过,定义类型时不会分配存储空间,当定义一个对象的时候,将为其分配存储空间。
 
当然,有时候人们也希望可以动态的为其分配内存,当不用的时候再销毁它,就有了如下定义方式:
Test *p;
p = new Test;
当不再使用此动态对象的时候,必须用delete:
delete p;
现在关心的应该是怎么通过对象调用类的成员?
访问对象中的成员有三种方法:
通过对象名和对象成员引用运算符 (.) 
通过指向对象的指针和指针成员引用运算符 (->)
通过对象的引用变量和对象成员引用运算符 (.) 
 
 
假定有一个Test类,类中有一个 公有的Sum()函数,则在外部调用Sum()的方法有:
Test test;
test.Sum();
 
Test *p;
p = new Test;
p->Sum();

Test test, &r = test;
r.Sum();
这些方式都是合法的。
 
 
 

4.构造函数与析构函数:

建立一个对象的时候,通常最需要做的工作就是初始化对象,如对数据成员赋初值,而构造函数就是用来在创建对象时初始化对象,为对象数据成员赋初值。为什么非得这么做呢?因为在类里面,数据成员不能够进行初始化。即:
class  Test
{
     int x = 0;
     ...
};
这样做是错误的。
想想为什么不可以,还是上面说过的,类只是定义了一个数据类型,不会占用存储空间,而在类里面对数据成员赋初值则会占用存储空间,因此自相矛盾。
 
如果数据成员是公有的,那么可以在类外直接对它初始化,但如果是私有的,那么就不能直接访问它,这就要用到构造函数。构造函数就是用来处理对象的初始化问题,构造函数是一种特殊的成员函数,不需要人为调用,而是在对象建立的时候自动被执行。
 
C++规定构造函数的名字要与类名保持一致,而且不能指定返回类型。请看下面程序:
#include <iostream>

using namespace std;

class Test
{
public:
    Test ();
    Test (int x,int y);
    void Sum();
private:
    int a,b;
};
Test::Test()
{

}
Test::Test(int x,int y)
{
    a=x;
    b=y;
}
void Test::Sum()
{
    cout<<a+b;
}
int main()
{
    Test test(3,4);
    test.Sum();
    return 0;
}
这里定义了两个构造函数 Test() 和 Test(int x,int y),由于创建对象一般是在类外部进行,因此构造函数声明为public。

第一个为无参构造函数或默认构造函数,写这个函数的好处是当你在创建对象的时候并不想立即对它初始化,而是在后续的工作中再进行赋初值,即:
Test test;
如果没有默认构造函数则会报错。那么问题来了,之前的例子根本没写构造函数,却不会报错,这是为什么?
 
因为在IDE里(ex:codeblocks)不会报错是因为IDE会自动生成一个默认构造函数。当然,如果你已经定义了一个有参的构造函数,它就不再为你自己生成一个默认构造函数,也就是说如果现在把这个Test类里的默认构造函数删除了,
Test test;
就会报错。
 

第二个构造函数就完成了我们的初始化工作,它有两个形参,分别给数据成员a,b进行初始化,定义对象的时候传入了 3和4,则 a和b 被初始化为 3 和 4 。因此程序运行的结果是 打印出了 7。
 
构造函数初始化列表
 
所谓初始化列表,它的功能和我们写在函数体里的赋初值是一样的,也就是说可以写成如下形式:
 
#include <iostream>

using namespace std;

class Test
{
public:
    Test ();
    Test (int x,int y);
    void Sum();
private:
    int a,b;
};
Test::Test()
{

}
Test::Test(int x,int y):a(x),b(y)
{

}
void Test::Sum()
{
    cout<<a+b;
}
int main()
{
    Test test(3,4);
    test.Sum();
    return 0;
}
你可以选择写的更简洁一点:
#include <iostream>

using namespace std;

class Test
{
public:
    Test () {}
    Test (int x,int y):a(x),b(y) {}
    void Sum();
private:
    int a,b;
};

void Test::Sum()
{
    cout<<a+b;
}
int main()
{
    Test test(3,4);
    test.Sum();
    return 0;
}
那么这样做和普通的赋值有区别吗?
当然是有的,对于一般的变量,两种都可行,但是如果需要初始化的是类类型的成员,则必须使用构造函数初始化列表。比如:
 
#include <iostream>

using namespace std;

class Test
{
public:
    Test () {}
    Test (int x,int y):a(x),b(y) {}
    void Sum();
private:
    int a,b;
};

void Test::Sum()
{
    cout<<a+b;
}
class AnotherTest
{
public:
    AnotherTest(int i,int j):test(i,j) {test.Sum();}
private:
    Test test;
};
int main()
{
    AnotherTest test(3,4);
    return 0;
}
 
之前说过,类的成员函数可以重载,带默认参数等,那么构造函数呢?
构造函数也是可以的,刚才那个例子就是构造函数的重载,默认构造和有参构造。

下面是一个带默认参数的构造:
#include <iostream>

using namespace std;

class Test
{
public:
    Test () {}
    Test (int x = 0,int y = 0):a(x),b(y) {}
    void Sum();
private:
    int a,b;
};

void Test::Sum()
{
    cout<<a+b;
}

int main()
{
    Test test(3);
    test.Sum();
    return 0;
}
一旦指定了 x = 0,就必须指定 y 的值。
 
Test (int x = 0,int y):a(x),b(y) {}
这样是错误的。


复制构造函数
 
复制构造函数也称为拷贝构造函数,它的作用是用一个已经生成的对象来初始化另一个同类的对象。
即实现如下功能:
Test test1(3,4);
Test test2 = test1;
复制构造函数的写法:
 
类名 (const 类名& obj)
{
       函数体
}
 
例如:
#include <iostream>

using namespace std;

class Test
{
public:
    Test () {}
    Test (int x ,int y):a(x),b(y) {}
    Test (const Test& t):a(t.a),b(t.b) {}
    void Sum();
private:
    int a,b;
};

void Test::Sum()
{
    cout<<a+b;
}

int main()
{
    Test test1(3,4);
    Test test2 = test1;
    test2.Sum();
    return 0;
}
程序运行的结果是 7 ,它完成了给test2进行初始化。
当然也可以用如下语句:
Test test2(test1);
深复制和浅复制:
 
如果不定义复制构造函数,以上对象也可以这样进行初始化,原因就是系统也会自己生成一个复制构造函数。
现在存在这样一个类:
#include <iostream>
#include <cstring>

using namespace std;

class Test
{
public:
    Test (int x,char *ptr)
    {
        a = x;
        p = new char [x];
        strcpy(p,ptr);
    }
    Test (const Test& C)
    {
         a = C.a;
         p = new char [a];
         p = C.p;
    }
    void Print();
private:
    int a;
    char *p;
};

void Test::Print()
{
    int i = 0;
    while(p[i] != '\0')
    {
        cout<<p[i];
        i++;
    }

}

int main()
{
    char p[5] = "test";
    Test a(10,p);
    Test b(a);
    b.Print();
    return 0;
}

 
因为对象 a 和 b 指向的是同一段内存区域,如果我们在完成复制(浅复制)之后删除了 a,它指向的内存区域同样也被删除了,而此时 b 此时仍然指向的是这片区域,如果再把 b 删除掉,同一片内存区域被释放两次,这明显是错误的。也就是说,浅复制只是简单的将 a 中p 的值给了 b 中的 p。那么要解决这个问题就得用到深复制:
Test (const Test& C)
    {
         a = C.a;
         p = new char [a];
         if(p != 0)
           strcpy(p,C.p);
    }
 
 
析构函数:
 
析构函数在类里起了一个“清理”的作用,比如类中有需要动态开辟内存的成员,而在程序结束之后我们需要释放内存,这时只要将释放内存的语句写在析构函数中,而系统在程序运行结束之后会自动执行析构函数,进行内存的释放以及对象的销毁。
以下是一个例子:
#include <iostream>
#include <cstring>

using namespace std;

class Test
{
public:
    Test (int x,char *ptr)
    {
        a = x;
        p = new char [x];
        strcpy(p,ptr);
    }
    Test (const Test& C)
    {
         a = C.a;
         p = new char [a];
         if(p != 0)
           strcpy(p,C.p);
    }
    ~Test()
    {
        delete (p);
        cout<<"p has been destroyed"<<endl;
    }
    void Print();
private:
    int a;
    char *p;
};

void Test::Print()
{
    int i = 0;
    while(p[i] != '\0')
    {
        cout<<p[i];
        i++;
    }
    cout<<endl;
}

int main()
{
    char p[5] = "test";
    Test a(10,p);
    Test b(a);
    b.Print();
    return 0;
}
程序运行结果如下:
 
由此可见,a 和 b 的析构函数都被调用了。 
 
 
 

5.友元机制:

C++提供了友元机制,允许一个类将其非公有成员的访问权限授予指定的函数或类。友元的声明只能在类定义的内部,因此,访问类非公有成员除了自身成员,还有友元。
有如下程序:
#include <iostream>
#include <cstring>

using namespace std;

class Test
{
public:
    Test (int a)
    {
        x = a;
    }
    ~Test()  //析构函数
    {

    }
    friend void Print(Test& a,Test& b);
private:
    int x;
};

void Print(Test& a,Test& b)
{
    cout<<a.x*b.x;
}

int main()
{
    Test a(10);
    Test b(3);
    Print(a,b);
    return 0;
}
输出结果为 30 ,完成了求两个对象内的数据之积。
 
下面介绍友元类:
#include <iostream>
#include <cstring>

using namespace std;

class B;  //类的前向声明
class A
{
public:
    A(){}
    ~A()  //析构函数
    {

    }
    void Print(B& a);
};

class B
{
public:
    B (int a)
    {
        x = a;
    }
private:
    int x;
    friend void A::Print(B& a);
};
void A::Print(B& a)
{
    cout<<a.x;
}
int main()
{
    B test1(3);
    A test2;
    test2.Print(test1);
    return 0;
}
 类A成功的访问了类B的私有成员,并且打印出来。输出结果为 3。

友元类的关系是单向的,即 A 是 B 的友元,B 不是 A 的友元,类 B 不能访问 A 的数据成员。此外,友元的关系不能传递或继承,类 B 是类 A 的友元,C 是 B 的友元,那么 C 不是 A 的友元,除非另外声明一次。
 

6.继承与派生:

 
继承是面向对象程序设计的一个重要特性,继承允许在原有类的基础上创建新的类,举个例子,现在有一个平行四边形类,而菱形类,矩形类,正方形类 都属于平行四边形类,它们有一个共同点,那就是需要两条边长来描述图形,如果不采用继承,我们需要在每个类中定义两个数据成员,那样会显得很繁琐。下面我们看一个计算矩形面积的例子:
#include <iostream>

using namespace std;

class Parallelogram
{
public:
    Parallelogram(int a,int b):length(a),width(b) {}
    int getLength(){return length;}
    int getWidth() {return width;}
private:
    int length,width;
};
class Rectangle : public Parallelogram  //公有继承
{
public:
    Rectangle(int a,int b):Parallelogram(a,b) {}  //先对基类中的数据成员进行初始化
    void Area()     //计算面积
    {
        cout<<getLength()*getWidth();
    }
};
int main()
{
    Rectangle r(3,4);
    r.Area();
    return 0;
}
首先说一下继承方式,c++提供了三中继承方式。
public(公有继承),基类中的公有和保护成员保持原属性,私有成员为基类私有。
private(私有继承),基类中的所有成员在派生类中都是私有的。
protected(保护继承),基类的公有成员和保护成员在派生类中成了保护成员,私有成员仍为基类私有。
 
现在我们说如何设计一个派生类:
①从基类接收成员,除了构造函数和析构函数,派生类会把全部的成员继承过来,这是没有选择的。
②调整基类成员的访问。
③修改基类成员,可以在派生类中声明一个与基类同名的成员,此操作会覆盖基类的同名成员。
④在定义派生类的时候定义新的成员,定义构造函数和析构函数,初始化的时候必须先将基类的成员初始化(因为并没有继承基类的构造函数)之后才可以对派生类的成员进行初始化。析构函数也一样,需要在派生类中释放基类的数据成员(调用基类的析构函数)。
 
上面这个程序定义了一个基类(平行四边形类),它含有两个数据成员,代表两个边长,而矩形在计算面积的时候需要两个边长的长度,也就是长和宽,因此继承平行四边形类,并且新添加了计算面积的函数,程序输出结果 12。
 
 
多重继承和虚基类:
C++还支持一个派生类同时继承多个基类。
多重继承派生类的定义:
 
class  派生类名 : 访问标号1 基类名1 , 访问标号2 基类名2 , ....

{

         成员列表

}

同样,派生类的构造函数初始化列表在调用基类构造函数也应该按定义时的先后次序。

接下来看个例子:

 

#include <iostream>

using namespace std;

class BaseOne
{
public:
    BaseOne() {cout<<"This is BaseOne"<<endl;}
    BaseOne(int a):data(a) {cout<<"BaseOne's data is "<<data<<endl;}
private:
    int data;
};

class BaseTwo
{
public:
    BaseTwo() {cout<<"This is BaseTwo"<<endl;}
    BaseTwo(int a):data(a) {cout<<"BaseTwo's data is "<<data<<endl;}
private:
    int data;
};

class BaseThree
{
public:
    BaseThree() {cout<<"This is BaseThree"<<endl;}
};

class Derive:public BaseOne,public BaseTwo,public BaseThree
{
public:
    Derive () {cout<<"This is Derive"<<endl;}
    Derive (int a,int b,int c,int d,int e):BaseOne(a),BaseTwo(b),dataOne(c),dataTwo(d),data(e)
    {cout<<"Derive's data is "<<data<<endl;}
private:
    BaseOne dataOne;
    BaseTwo dataTwo;
    int data;
};

int main()
{
    Derive r1;
    cout<<endl;
    Derive r2(1,2,3,4,5);
    return 0;
}

 

程序运行结果如下:

 

在调用派生类的默认构造函数时,即使没有写出调用基类的默认构造函数,系统也会调用基类的默认构造函数,而在结果的第4 , 5行还调用了一次,原因是派生类里有两个基类的数据成员,因此我们可以观察到,程序先调用了基类的构造函数,然后调用派生类中子对象的构造函数,最后调用派生类的构造函数。

在调用构造函数的时候,先调用基类的构造函数,虽然BaseThree没有参数,但是仍然会调用它的构造函数,然后初始化子对象,调用构造函数,最后调用派生类的构造函数。

 

二义性问题:

假定我们有如下程序:

 

#include <iostream>
#include <cstring>

using namespace std;

class A
{
public:
    void fun() {cout<<"This is A"<<endl;}
};

class B
{
public:
    void fun() {cout<<"This is B"<<endl;}
};

class C:public A,public B
{
public:
    void hun() {fun();}  //产生二义性
};

int main()
{
    C c;
    c.hun();
    return 0;
}

 

相信看到这里大家都知道二义性问题的产生原因了吧,就是两个基类存在名称相同的数据成员,而派生类在调用基类数据成员的时候如果没有显式的指出它属于谁,那么程序就会产生错误,现在做如下修改:

 

 

void hun() {A::fun(); B::fun();}

 

这次程序会分别调用 A 和 B 的 fun() 函数。我们要做的只是在它前面写上 基类名加上域运算符 :: ,当然也可以通过 对象名.基类名 :: 和 对象指针名.基类名 :: 这两种方式。

 

 

虚基类:

假定现在有这样一种继承关系:

 

#include <iostream>

using namespace std;

class A
{
public:
    void fun() {cout<<"This is A"<<endl;}
};

class B:public A
{
public:
    void gun() {cout<<"This is B"<<endl;}
};

class C:public A
{
public:
    void hun() {cout<<"This is C"<<endl;}
};

class D:public B,public C
{
public:
    void kun() {fun();} //产生二义性
};

int main()
{
    D d;
    d.kun();
    return 0;
}

 

A 是基类,B 是 A 的派生类,C 也是 A 的派生类,而 D 是 B 和 C 的派生类,因此 D 可以访问 A 的数据成员,但现在会产生二义性问题,我们必须显式的指出 fun() 是来自 B 的 还是来自 C 的,但是我们都知道它来自 A , 因此我们希望找到一种方式,使得在继承间接共同基类时只保留一份成员,这就用到了虚基类的机制。

 

虚基类是在派生类定义时,指定继承方式时声明的。声明的一般形式:

 

class  派生类名 : virtual  访问标号 虚基类名 , ...

{

        成员列表

 

为了保证虚基类在派生类中只继承一次,应当在所有直接派生类中声明为虚基类。依然是上面那个程序,只需要:

 

class B:virtual public A

 

class C:virtual public A

 

这样在类 D 中调用 fun() 函数,就不用指出它究竟属于谁。

 

接下来看看虚基类构造函数和析构函数的一些特性。

有这样一个程序:

 

#include <iostream>

using namespace std;

class A
{
public:
    A() {cout<<"This is Grandpa"<<endl;}
    A(int a):One(a) {cout<<"Grandpa is "<<One<<" years old"<<endl;}
    ~A() {cout<<"A is over"<<endl;}
private:
    int One;
};

class B:virtual public A
{
public:
    B() {cout<<"This is father"<<endl;}
    B(int a,int b):A(a),Two(b) {cout<<"father is "<<Two<<" years old"<<endl;}
    ~B() {cout<<"B is over"<<endl;}
private:
    int Two;
};

class C:virtual public A
{
public:
    C() {cout<<"This is mother"<<endl;}
    C(int a,int b):A(a),Three(b) {cout<<"mother is "<<Three<<" years old"<<endl;}
    ~C() {cout<<"C is over"<<endl;}
private:
    int Three;
};

class D:public B,public C
{
public:
    D() {cout<<"This is me"<<endl;}
    D(int a,int b,int c,int d):A(a),B(a,b),C(a,c),Four(d) {cout<<"I am "<<Four<<" years old"<<endl;}
    ~D() {cout<<"D is over"<<endl;}
private:
    int Four;
};

int main()
{
    D d1;
    cout<<endl;
    //D d2(65,40,39,13);
    return 0;
}

首先看默认构造函数(先把d2注释掉),程序运行结果如下:

程序会自动调用构造函数,首先调用虚基类的构造函数,然后再根据派生类继承的次序调用构造函数,如果先继承了 C 类,那么先调用 C 类的构造函数。析构函数的调用则正好相反。

 

接下来看有参的,给 d1 加上注释,去掉 d2 的注释,程序运行结果如下:

调用次序是一样的,因此虚基类的构造函数优先于非虚基类的构造函数进行执行,如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有的派生类里(直接和间接)中,都必须通过构造函数的初始化列表对其进行初始化,在最后的派生类中不单单对直接继承的类进行初始化,还要对虚基类进行初始化。

 

 

7.多态性和虚函数:

 

首先介绍一下多态性,多态是指同样的消息被不同类型的对象接收时导致不同的行为,举个通俗易懂的例子,假定现在有一个模具,这个模具是一个人型模具,根据倒入里面金属液体的不同,它最终会形成不同类型的器件,如果倒入的是液体黄金,那么它会形成一个小金人,如果倒入的是铁水,那就会形成一个小铁人,多态大概就是这样的意思。

 

多态性可以通过很多方法实现,而我们要说的是 包含多态 实现多态性,C++采用虚函数实现包含多态,至少含有一个虚函数的类成为多态类。

在介绍虚函数之前,我们介绍两种联编。联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数分配内存地址,并且对外部访问也分配正确的内存地址。

在编译阶段就将函数实现和函数调用绑定起来称为静态联编,程序运行的时候才进行函数实现和函数调用的绑定称为动态联编。

举个例子:

 

#include <iostream>

using namespace std;

class A
{
public:
    void fun() {cout<<"Use A"<<endl;}
};

class B : public A
{
public:
    void fun() {cout<<"Use B"<<endl;}
};

int main()
{
    B b;
    A *p = &b;
    p->fun();
    return 0;
}

 

我们声明了一个指向类 B 的指针,但是程序的输出结果是:

 

之所以会这样是因为将其定义为 A 类型,程序在编译阶段就已经确定 A 类型的指针调用的 fun() 是 A 类的成员。

接下来看动态联编:

给刚才类 A 的 fun() 函数前面加上 virtual ,

 

virtual void fun() {cout<<"Use A"<<endl;}

将其定义为了虚函数,再次运行:

当编译器编译含有虚函数的类时,将为它建立一个虚函数表,相当于一个指针数组,存放每个虚函数的入口地址,编译器为该类增加一个额外的数据成员,这个数据成员是一个指向虚函数表的指针,通常称为vptr。这个例子中,A 类有一个虚函数 fun() , 所以虚函数表里只有一项,如果派生类没有重写这个虚函数,那么虚函数表里的元素所指向的地址就是基类虚函数的地址,重写之后,则 vptr 指向派生类的虚函数地址。

派生类可以继承基类的虚函数表,而且只要和基类同名的成员函数,无论前面加不加 virtual ,都会自动成为虚函数,虚函数的调用规则是,根据当前对象,优先调用对象本身的成员函数。

 

虚析构函数:

如果将基类的析构函数声明为虚函数,那么其派生类的析构函数也变为虚析构函数,即使名字不同,当基类的析构函数是虚析构函数时,无论指针指的是同一类族中的哪一个类对象,系统总会采用动态联编,调用正确的析构函数,对该对象进行清理。C++中,不支持虚构造函数。

 

纯虚函数:

许多情况下,不能在基类中为虚函数给出一个有意义的定义,那就将其声明为纯虚函数,具体怎么实现交给派生类去做,纯虚函数的定义形式为:

 

virtual   返回类型  函数名 (形式参数列表) = 0;

 

纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对其进行定义,如果一个类里声明了虚函数,而在其派生类中没有对该函数定义,那么该函数在派生类中仍然为纯虚函数。含有纯虚函数的类成为抽象类,抽象类不能定义对象,如果派生类里给出了抽象类中纯虚函数的实现,那么该派生类不再是抽象类,否则仍然是抽象类。抽象类至少含有一个纯虚函数。

 

接下来看一个计算圆形面积和圆柱体体积的程序:

 

#include <iostream>

using namespace std;

class Sharp
{
public:
    virtual double area() = 0;
    virtual double volumn() = 0;
};

class Circle : public Sharp
{
public:
    Circle(double r):R(r) {}
    virtual double area() {return 3.1415926*R*R;}
    virtual double volumn() {return 0;}
private:
    double R;
};

class Cylinder : public Circle
{
public:
    Cylinder(double a,double b):Circle(a),H(b) {}
    virtual double volumn() {return area()*H;}
private:
    double H;
};

int main()
{
    Circle a(20.0);
    Cylinder b(10.0,2.0);
    cout<<a.area()<<endl;
    cout<<b.volumn()<<endl;
    return 0;
}

 

程序运行结果:

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++类的介绍 的相关文章

  • 使用内部构造函数实例化类

    我有一个类 其构造函数被定义为内部 这意味着我无法实例化它 虽然这可能有道理 但出于调试和研究目的 我仍然愿意做一次 是否可以通过反射来做到这一点 我知道我可以访问私有 内部成员 但是我可以调用内部构造函数吗 或者 由于构造函数没有做任何重
  • 实体框架中的重复键异常?

    我试图捕获当我将具有给定用户名的现有用户插入数据库时 引发的异常 正如标题所说 我正在使用 EF 当我尝试将用户插入数据库时 引发的唯一异常是 UpdateException 如何提取此异常以识别其是否是重复异常或其他异常 catch Up
  • ProtoBuf-net AsReference 需要 Activator.CreateInstance 中的公共构造函数吗?

    在我的两门课程中 看起来像这样 最少 using System using System Collections Generic using System Collections using System ComponentModel us
  • 从 .Net 将简单数据插入 Excel 文件的最简单方法

    我有一个 Excel 文件 大约有 10 列和 1 20 行 我需要插入 1 20 行包含各种数据元素 我想知道是否有一种方法可以将一些标签放入 Excel 文件中 以便可以找到并替换它们 将列标记为 名称 的东西 这样我就可以在代码中说
  • C/C++ 中随机数生成器的实现[重复]

    这个问题在这里已经有答案了 我对 C 中随机数生成器的实现有点困惑 它也与 C 中的明显不同 如果我理解正确 对 srand seed 的调用会以某种方式初始化可通过 rand 访问的隐藏变量 种子 该变量又将函数指向预先生成的序列 例如例
  • 隐形打开的弹出窗口

    第二天就解决这个问题 要重现 请创建新的 WPF 应用程序 xaml
  • 首先EntityFramework数据库 - 类型映射 - 将binary(8)从SQL映射到C#中的int

    在 SQL 内部 我有一个主键为二进制 8 的表 当我使用该表添加到我的模型中时Update Model from Database我可以看到该列有 type Binary 在 C 中 我将该列设为byte 我可以将该列映射到 int 吗
  • Type_traits *_v 变量模板实用程序顺序无法编译

    看过了这个答案 https stackoverflow com a 31763111 7151494 我试图想出一个变量模板从中获取代码的实用程序 template
  • 使用正则表达式匹配以“Id”结尾的单词?

    如何组合一个正则表达式来匹配以 Id 结尾的单词并进行区分大小写的匹配 试试这个正则表达式 w Id b w 允许前面的单词字符Id和 b确保Id位于单词末尾 b是字边界断言
  • 使用 Microsoft Graph 创建用户

    如何使用 Microsoft graph 创建用户 因为我在保存过程中遇到了权限失败的问题 我确实有几个问题 在图中调用创建用户 API 将在哪里创建用户 是在 Azure AD 还是其他地方 我尝试通过传递 json 和必需的标头来调用创
  • IClaimsTransformation 未触发

    我尝试过实施一个IClaimsTransformation我在 ASP NET CORE 3 1 Web 应用程序中找到的类 public class ClaimsTransformer IClaimsTransformation publ
  • 使用对象列表构建树

    我有一个带有属性 id 和parent id 的对象列表 我想建造一棵树来连接那些孩子和父母 1 个父对象可以有多个子对象 并且有一个对象将成为所有对象的祖先 实现该功能最快的算法是什么 我使用 C 作为编程语言 但其他语言也可以 像这样的
  • .NET 的 HttpWebResponse 是否会自动解压缩 GZiped 和 Deflated 响应?

    我正在尝试执行一个接受压缩响应的请求 var request HttpWebRequest HttpWebRequest Create requestUri request Headers Add HttpRequestHeader Acc
  • 如果数组为空,LINQ 返回 null

    public class Stuff public int x other stuff 我有一个IEnumerable
  • 如何使用eclipse构建C++应用程序

    我已经从以下位置下载了 Eclipse Juno for C here http www eclipse org downloads download php file technology epp downloads release ju
  • 停止 TcpListener 的正确方法

    我目前正在使用 TcpListener 来处理传入连接 每个连接都有一个线程用于处理通信 然后关闭该单个连接 代码如下 TcpListener listener new TcpListener IPAddress Any Port Syst
  • 无效的模板相关成员函数模板推导 - 认为我正在尝试使用 std::set

    我有一个继承自基类模板的类模板 基类模板有一个数据成员和一个成员函数模板 我想从我的超类中调用它 我知道为了消除对成员函数模板的调用的歧义 我必须使用template关键字 我必须明确引用this在超级班里 this gt base mem
  • 编译器什么时候内联函数?

    在 C 中 函数仅在显式声明时才内联inline 或在头文件中定义 或者编译器是否允许内联函数 因为他们认为合适 The inline关键字实际上只是告诉链接器 或告诉编译器告诉链接器 同一函数的多个相同定义不是错误 如果您想在标头中定义函
  • 请解释为什么Java和C对此代码给出不同的答案

    public class Test public static void main String args int i 10 i i System out println value of i is i 输出是 10 当我在中执行类似的代码
  • XmlDocument Save 使文件保持打开状态

    我有一个简单的 C 函数 可以创建一个基本的 XML 文件并保存 private void CreateXMlFile string Filename string Name string Company XmlDocument doc n

随机推荐

  • STM32—ADC详解入门(ADC读取烟雾传感器的值)

    目录 一 ADC是什么 二 ADC的性能指标 三 ADC特性 四 ADC通道 五 ADC转换顺序 六 ADC触发方式 七 ADC转化时间 八 ADC转化模式 九 实验 使用ADC读取烟雾传感器的值 1 配置 2 代码 一 ADC是什么 AD
  • MSYS2更换国内源

    今天安装了Msys64 但是Msys64使用的国外源实在太慢 必须更新为国内源 目前测试过国内最快是清华大学的源 我的安装路径为d msys64 为什么要安装在D盘 因为msys64需要不断更新数据和安装自己的软件 也就是说会占用越来越多的
  • 使用mnist数据集实现手写字体的识别

    1 MNIST是一个入门级的计算机视觉数据集 它包含各种手写数字图片 它也包含每一张图片对应的标签 告诉我们这个是数字几 该数据集包括60000行的训练数据集 mnist train 和10000行的测试数据集 mnist test 每一张
  • mysql索引左侧原则,你真的了解吗?

    前言 写这篇文章源自一位杠精同事提了个问题 左侧原则跟where条件顺序有无关系 我想了想 好像是有关系的 不敢确定 但是自己又懒得动手测试 于是发起ETC自动抬杠功能 强行杠了一拨 结果杠输了 接下来即是动手验证 预习执行计划 实践 咱们
  • Something about C

    What the hell How long since I touch with C What a pity I have to work with it now Global variable Better define a globa
  • c++中setw()与setfill()的用法详情

    c 中setw 与setfill 的用法详情 在C 中 setw int n 用来控制输出间隔 例如 cout lt lt s lt
  • 关于自搭网站XAMPP(三)MYSQL增删改查(水)

  • Ubuntu 18.04下载安装以及分区教程

    收藏一下写的超赞的博客 Ubuntu18 04安装教程 超详细的图文教程 安装Ubuntu Linux系统时硬盘分区最合理的方法ubuntu18 04分区设置 也安装过很多次ubuntu了 记录一下最重要的踩坑点 分区完成后会让选择安装启动
  • 算法之字符串匹配一

    目录 前言 BF算法 RK算法 总结 参考资料 前言 字符串匹配指的是一个短点的字符串与一个长点的字符串进行匹配 并确认短的字符串是否在长的字符串中存在 匹配算法有很多 本文介绍两种简单 容易理解的两种算法 BF算法和RK算法 BF算法 B
  • C语言版本数据结构03:顺序表+链表

    今天我们来学习数据结构的第一个顺序结构 顺序表和链表 1 线性表 线性表是我们要学的第一个结构 我们知道一串连续的数字或者字符想要在内存中存储可以用数组 但是我们又知道数组是静态的 那么如果我想要对这组数据进行增删查改呢 这就显现出线性表的
  • Mysql按数字大小排序String字段

    今天做项目的时候 遇到个小小的问题 在数据库中查询的时候 要用String类型的数字进行一下排序 结果是按照字符串排序来处理的 没有达到预想中的效果 于是先是想到将字符转成数字型的 在网上搜了一下 基本上都是这样做的 只不过很多人实现的方式
  • 内存检测工具Dr. Memory的使用

    Dr Memory是一个内存调试工具 它是一个开源免费的内存检测工具 它能够及时发现内存相关的编程错误 比如未初始化访问 内存非法访问 数组越界读 写 以及内存泄露等 它可以在Linux Windows Mac OS和Android操作系统
  • ngrok的使用(超详细)

    1 ngrok简介 百度百科 ngrok 是一个反向代理 通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道 ngrok 可捕获和分析所有通道上的流量 便于后期分析和重放 啥玩意 1 其实说白了就是你写一个项目 在PC上完美
  • 微信小程序合法域名配置

    在微信小程序的开发过程中 当需要请求第三方网站数据时 各种教程就直接说调用wx request接口即可 但是当初学者自己用的时候就会出现问题 比如我们这里请求聚合数据的API 里边有不少免费的数据申请就可以使用 调用邮编查询的接口 getP
  • Java中的wait()与notify()/notifyAll()

    1 wait 与sleep yield 的不同 调用sleep 和yield 时 线程掌握的对象上的锁不会被释放 而调用wait 时 线程掌握的对象上的锁会被释放掉 这一点是需要注意的 也是有意义的 因为调用wait 会释放锁 所以在一个s
  • 按键从Linux到Android

    按键从Linux到Android 现在的普通按键也集成到Linux Input子系统中了 只需要把按键对应的IO端口配置好 按键就可以工作了 所以一般提供的BSP 或者叫作解决方案 中 已经完善了按键驱动 关键是快速的了解按键的映射 所以这
  • objective c 中的继承和多态简单示意(二)

    holydancer原创 如需转载 请在显要位置注明 转自holydancer的CSDN专栏 原文地址 http blog csdn net holydancer article details 7334377 OC中的继承和JAVA C
  • 剑指Offer第四十九题:把字符串转换成整数

    题目描述 将一个字符串转换成一个整数 实现Integer valueOf string 的功能 但是string不符合数字要求时返回0 要求不能使用字符串转换整数的库函数 数值为0或者字符串不是一个合法的数值则返回0 思路 判断正负什么的都
  • LeGO-LOAM论文翻译(内容精简)

    LeGO LOAM是一种在LOAM之上进行改进的激光雷达建图方法 建图效果比LOAM要好 但是建图较为稀疏 计算量也更小了 本文原地址 wykxwyc的博客 github注释后LeGO LOAM源码 LeGO LOAM NOTED 关于代码
  • C++类的介绍

    最近在学习SLAM 顺便将C 类的知识复习一下 其中部分官方定义和程序设计方法来源于西北工业大学魏英老师 1 类的定义 是用户自定义的数据类型 C 一个类定义的形式如下 class 类名 成员列表 成员列表是类成员的集合 数目可以任意多 一