C++类的介绍

最近在学习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<
这是一个设置了默认参数的函数,但是很遗憾,这是错误的,下面这样则是正确的:
class Test
{
public:
    void Sum(int a=0,int b=0);
};
void Test::Sum(int a,int b)
{
    cout<
原因是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 

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<
这里定义了两个构造函数 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 

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<
你可以选择写的更简洁一点:
#include 

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<
那么这样做和普通的赋值有区别吗?
当然是有的,对于一般的变量,两种都可行,但是如果需要初始化的是类类型的成员,则必须使用构造函数初始化列表。比如:

#include 

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<

我们之前说过,类的成员函数可以重载,带默认参数等,那么构造函数呢?
构造函数也是可以的,刚才那个例子就是构造函数的重载,默认构造和有参构造。

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

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<
一旦指定了 x = 0,就必须指定 y 的值。

Test (int x = 0,int y):a(x),b(y) {}
这样是错误的。


复制构造函数

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

类名 (const 类名& obj)
{
       函数体
}

例如:
#include 

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<
程序运行的结果是 7 ,它完成了给test2进行初始化。
当然也可以用如下语句:
Test test2(test1);
深复制和浅复制:

如果我们不定义复制构造函数,以上对象也可以这样进行初始化,原因就是系统也会自己生成一个复制构造函数。
现在存在这样一个类:
#include 
#include 

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<


因为对象 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 
#include 

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"<
程序运行结果如下:

由此可见,a 和 b 的析构函数都被调用了。 



5.友元机制:

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

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<
输出结果为 30 ,完成了求两个对象内的数据之积。

下面介绍友元类:
#include 
#include 

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成功的访问了类B的私有成员,并且打印出来。输出结果为 3。

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

6.继承与派生:


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

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<
首先说一下继承方式,c++提供了三中继承方式。
public(公有继承),基类中的公有和保护成员保持原属性,私有成员为基类私有。
private(私有继承),基类中的所有成员在派生类中都是私有的。
protected(保护继承),基类的公有成员和保护成员在派生类中成了保护成员,私有成员仍为基类私有。

现在我们说如何设计一个派生类:
①从基类接收成员,除了构造函数和析构函数,派生类会把全部的成员继承过来,这是没有选择的。
②调整基类成员的访问。
③修改基类成员,可以在派生类中声明一个与基类同名的成员,此操作会覆盖基类的同名成员。
④在定义派生类的时候定义新的成员,定义构造函数和析构函数,初始化的时候必须先将基类的成员初始化(因为并没有继承基类的构造函数)之后才可以对派生类的成员进行初始化。析构函数也一样,需要在派生类中释放基类的数据成员(调用基类的析构函数)。

上面这个程序定义了一个基类(平行四边形类),它含有两个数据成员,代表两个边长,而矩形在计算面积的时候需要两个边长的长度,也就是长和宽,因此继承平行四边形类,并且新添加了计算面积的函数,程序输出结果 12。


多重继承和虚基类:
C++还支持一个派生类同时继承多个基类。
多重继承派生类的定义:

class  派生类名 : 访问标号1 基类名1 , 访问标号2 基类名2 , ....

{

         成员列表

}

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

接下来我们看个例子:

#include 

using namespace std;

class BaseOne
{
public:
    BaseOne() {cout<<"This is BaseOne"<

程序运行结果如下:

C++类的介绍_第1张图片

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

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


二义性问题:

假定我们有如下程序:

#include 
#include 

using namespace std;

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

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

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

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


虚基类:

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

#include 

using namespace std;

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

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 

using namespace std;

class A
{
public:
    A() {cout<<"This is Grandpa"<

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

C++类的介绍_第2张图片

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


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

C++类的介绍_第3张图片

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


7.多态性和虚函数:


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

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

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

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

我们举个例子:

#include 

using namespace std;

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

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


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

接下来我们来看动态联编:

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

virtual void fun() {cout<<"Use A"<

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


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

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

 

虚析构函数:

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


纯虚函数:

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


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


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


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

#include 

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<

程序运行结果:


你可能感兴趣的:(C++)