C/C++面对对象

目录

1. 面对对象基本概念

1.1 面向过程与面向对象的区别

1.2 面对对象的基本特征

2. 类的声名

2.0 类和结构体的区别

2.1 类中静态数据成员与静态成员函数

2.2 const 修饰符在类中的用法

2.3 友元函数和有元类

3. 构造函数和析构函数

3.0 只有当类中没有任何构造函数时,系统才会生成一个无参的构造函数。

3.1 默认拷贝构造函数对数据成员作浅拷贝,一旦涉及指针、堆空间申请等操作,或需手动实现深拷贝。  

3.2 初始化列表和嵌入类的初始化

3.3 实现一个最基本的String类

4. 函数重载

5. 运算符重载

5.0 基本概念

5.1 重载前自增和后自增运算符

6. 继承

6.0 继承和组合的区别

7. 虚继承

7.0 虚继承的作用

7.1 虚继承中构造函数的调用

7.2 计算虚继承中对象占用的空间

8. 多态与虚函数

8.0 虚函数是实现多态的手段

8.1 虚函数表的概念

8.2 纯虚函数于抽象类


1. 面对对象基本概念

1.0 对象的两个要素是属性和方法。将相同或相近的对象抽象出来就形成了类。

1.1 面向过程与面向对象的区别

面向过程是分析问题解决的步骤,明确每个步骤的输入和输出以及个流程,是一种结构化的自上而下的程序设计方法。

面对对象是把构成问题的事务分解成对象,以数据为核心,以构建类为主要工作的自上而下程序设计方法。

1.2 面对对象的基本特征

首先类是抽象的。

封装性:把自己的属性和方法隐藏起来。

继承性:代码复用。

多态性:多态是指不同对象对于同样的消息做出不同的响应。程序中通过运行时绑定实现多态。例如,一个父类指针只有在运行时菜知道自己实际绑定的对象类型,程序可以将子类对象地址赋值给父类指针,在调用方法时,根据具体的对象类型执行相应子类中的方法。

2. 类的声名

2.0 类和结构体的区别

类class中默认访问权限时private,结构体struct时public。

2.1 类中静态数据成员与静态成员函数

示例代码:

class Trade
{
private:
    double _amount;
    static double _fee;        //#1
public:
    Trade(double amount) { this->_amount = amount; }
    double getDirty() { return _amount; }
    static double getFee() { return _fee; }            //#3
    double getClean() { return _amount *(1 - _fee / 100); }  //#4
};
double Trade::_fee = 0.08;   //#2

int main()
{
    double x;
    x = Trade::getFee();  //#方式1

    Trade tr(10);
    x = tr.getFee();            //#方式2

    Trade * tp = new Trade(10);
    x = tp->getFee();        //#方式3

    cout << x << endl;
}

代码中使用static修饰符声名了一个静态数据成员:

 static double _fee;        //#1

 静态数据成员属于整个类,不属于某一个对象,静态数据只有唯一的一份拷贝,所有对象共享静态数据成员。没有使用static修饰的数据成员在每个对象中都有自己的一份。

静态数据成员必须在类内部声名,在类外初始化。

 double Trade::_fee = 0.08;   //#2

程序中定义的静态成员函数:

 static double getFee() { return _fee; }            //#3

静态成员函数也同样属于类本身,不属于某一个对象。静态成员函数和普通函数最大的区别在于,静态成员函数没有this指针。普通函数调用时,会将对象的地址隐式的作为第一参数,也就this指针。既然静态成员函数中没有this指针,那么在静态函数中也就不能访问非静态成员了,因为非静态成员都是通过this指针访问的。换句话说:在静态函数中只能访问静态成员,包括静态数据成员和静态成员函数。

静态成员函数的调用有如上的三种方式,但实际上方式2和方式3会隐式转换为方式1的形式。

2.2 const 修饰符在类中的用法

示例代码:

class Test
{
private:
    const int _num;        //#1
public:
    Test(int n):_num(n) { }   //#2
    int getNum() const { return _num; }  //#3
};

int main()
{
    int x = 10;
    Test t(x);
    x = t.getNum();
    cout << x << endl;
}

 类中声名了唯一的一个const修饰的变量_num,需要注意的是,必须通过构造函数的初始化列表初始化const成员。初始化后的值不能修改。不能在构造函数体内初始化,因为初始化不同于赋值,构造函数中的赋值语句相当于改变了初始化列表在初始化时的默认值,这与const修饰符的作用相悖。

 Test(int n):_num(n) { }   //#2 

在成员函数后加上const修饰符表示不能在函数中修改类的数据成员。

 int getNum() const { return _num; }  //#3

2.3 友元函数和有元类

友元函数不是类中的成员函数,而是类外部的函数。友元函数能够访问类的非公有成员。

class Test
{
private:
    int _num;
public:
    Test(int num):_num(num){}
    friend int getNum(const Test &t);
};
int getNum(const Test &t)
{
    return t._num;
}

int main()
{
    Test t(10);
    cout << getNum(t) << endl;
}

 有缘类与友元函数类似,可以将一个类A声名为类一个类B的友元,这样类B就可以访问类A的非公有成元。唯一需要注意的是友元关系不能继承。

class A
{
private:
    int _num;
public:
    A(int num):_num(num){}
    friend class B;
};

class B
{
public:
    int getNum(const A &a) { return a._num; }
};

int main()
{
    A a(10);
    B b;
    cout << b.getNum(a) << endl;

3. 构造函数和析构函数

3.0 只有当类中没有任何构造函数时,系统才会生成一个无参的构造函数。

 如一下对象的定义将会出错:

class Test
{
private:
    int _num;
public:
    Test(int num) : _num(num) {}
};

int main()
{
    Test t;   //错误
}

解决办法是在类中声名无参的构造函数:

class Test
{
private:
    int _num;
public:
    Test(): _num(0){}
    Test(int num) : _num(num) {}
};

int main()
{
    Test t;   //正确
}

3.1 默认拷贝构造函数对数据成员作浅拷贝,一旦涉及指针、堆空间申请等操作,或需手动实现深拷贝。  

示例代码:

class Test
{
private:
    int * _p;
public:
    Test(int num) : _p(NULL)
    {
        _p = (int *)malloc(sizeof(int));
        *_p = num;
    }
    ~Test() { free(_p); _p = NULL; }
    int getNum() const { return *_p; }
};

int main()
{
    Test * t = new Test(10);
    Test *T = new Test(*t);
    delete T;
    int x = t->getNum();
    cout << x << endl;
}

 上面代码中在Test类中定义了一个整型指针,并在构造函数中申请了一个整型单位空间。主函数的第一行定义了一个指针对象t,并初始化值10,第二行定义对象T时使用了对象t进行初始化。因为类中没有拷贝构造函数,所以系统将自动生成默认的拷贝构造函数并且进行浅拷贝,其原型如下:

Test(const Test &obj) { this->_p = obj._p;}

 可以看出,所谓浅拷贝是将源对相成员指针的值直接赋值给了当前对象指针成员。这就导致了对象t 和 T 的指针成员指向了同一块空间,这块空间是 对象 t 定义的时候构造的。主函数第三行销毁了对象 T ,导致对象T调用析构函数free掉了这块空间,所以当通过对象 t 获取指针 _p 的指向的内容时,得到的看似一个随机值,因为此时对象 t 指针 _p 已经是一个野指针。

解决办法是在类中定义一个深拷贝构造函数:

Test(const Test &t)

{
        _p = (int *)malloc(sizeof(int));
        *_p = *(t._p);
 }

3.2 初始化列表和嵌入类的初始化

类中所有的数据成员都会在执行构造函数体前,通过构造函数的初始化列表的初始化操作,不论数据成员是否显示出现在初始化列表中。

如果想在初始化类中的嵌入类对象成员,必须在构造函数的初始化列表中进行初始化,构造函数体内只能使用赋值操作。

3.3 实现一个最基本的String类

class TString
{
private:
    char * _data;
public:
    TString()
    {
        _data = new char[1];
        _data[0] = '\0';
    }
    TString(const char * str)
    {
        if (str == NULL)
        {
            _data = new char[1];
            _data[0] = '\0';
        }
        else
        {
            _data = new char[strlen(str) + 1];
            strcpy(_data, str);
        }
    }
    TString(const TString &str)
    {
        _data = new char[strlen(str._data) + 1];
        strcpy(_data, str._data);
    }
    TString & operator=(const TString &str)
    {
        if (&str == this)
            return *this;
        delete[] _data;
        _data = new char[strlen(str._data) + 1];
        strcpy(_data, str._data);
        return *this;
    }
    virtual ~TString()
    {
        delete[] _data;
    }
};

4. 函数重载

  1. 函数重载是指在同一作用域内,一组具有不同参数列表的同名函数。
  2. 函数的返回值类型和函数重载没有关系。
  3. C++编译过程中会对函数重新命名,而C则没有,所以C++支持函数重载。
  4. C++中函数重命名 = 返回值类型 + 作用域 + 原始函数名 + 参数列表。
  5. 如果直接在C++程序中调用C函数,会发生连接错误(LINK ERROR),因为C++编译中会重命名函数而C不是。这时候需要在C函数前面使用 extern C 告诉C++编译器。
  6. 名字隐藏 : 指父类有一组重载函数,子类在继承父类时如果覆盖了这组函数中任何一个,则其余没有被覆盖的函数在子类中是不可见的。一中解决方法是全部覆盖父类函数,另外一中是不覆盖父类函数,另起一个函数名。

5. 运算符重载

5.0 基本概念

运算符重载的方式有两种:类成员函数和友元函数。一般来说,单目运算符使用类成员方式,双面运算符使用友元函数,但是()和 [ ] 必须使用成员函数方式, << 和 >> 必须使用友元函数

如下运算符不能重载 : 成员访问运算符 . ,指针运算符 *, 域运算符 :: ,长度运算符sizeof ,和条件运算符 ?。

5.1 重载前自增和后自增运算符

class Step
{
private:
    int _num;
public:
    Step(int num) { this->_num = num; }
    int getStep() { return _num; }
    Step & operator++()            //重载前自增
    {
        _num++;
        return *this;
    }
    Step & operator++(int)            //重载后自增
    {
        Step tmp = *this;
        ++*this;
        return tmp;
    }
};

int main()
{
    Step step(10);
    ++step;
    cout << step.getStep() << endl;
    Step x = step++;
    cout << x.getStep() << endl;
    cout << step.getStep() << endl;
}

6. 继承

6.0 继承和组合的区别

继承是si-a的关系,组合是has-a的关系。

7. 虚继承

7.0 虚继承的作用

在使用虚继承时,通过virtual关键字修饰符修饰继承关系,虚继承中的父类称为虚基类。使用虚继承后,避免了访问二义性的问题。通过类D的对象访问数据成员a时,编译器不会报错,因为虚继承保证了D中只有类A的一份拷贝。

class A { public: int a; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

int main()
{
    D d;
    cout << d.a << endl;

7.1 虚继承中构造函数的调用

 示例代码

class A
{
public:
    char ch1;
    A() { ch1 = 'A'; }
    A(char ch1) { this->ch1 = ch1; }
};

class B : public virtual A
{
public:
    char ch2;
    B() { ch2 = 'B'; }
    B(char ch1, char ch2) : A(ch1) { this->ch2 = ch2; }
};

class C : virtual public A
{
public:
    char ch3;
    C() { ch3 = 'C'; }
    C(char ch1, char ch3) :A(ch1) { this->ch3 = ch3; }
};

class D : public B, public C
{
public:
    char ch4;
    D() { ch4 = 'D'; }
    D(char ch1, char ch2, char ch3, char ch4) :B(ch1, ch2), C(ch3, ch3) { this->ch4 = ch4; }
};

int main()
{
    D d('a','b','c','d');
    cout << d.ch1 << d.ch2 << d.ch3 << d.ch4 << endl;
}

上述代码的输出如下

Abcd
请按任意键继续. . .

在看待上述结果之前需要明白: 第一,单继承中,在执行子类构造函数体前一定要在初始化列表中先执行父类的构造函数,如果子类没有显示写出来,则调用默认的无参父类构造函数。第二,但凡虚继承关系的,虚基类只被初始化一次,所谓初始化一次是指在在子类构造函数初始化列表中第一步所作的事。第三,在菱形继承中,也就事如上示例代码的继承中,底层的构造函数初始化列表会首先调用顶层类的构造函数。

结合以上三点,主函数对象 d 定义时的构造函数等价于:

D(char ch1, char ch2, char ch3, char ch4) : A(),B(ch1, ch2), C(ch3, ch3) { this->ch4 = ch4; }

可以看出,首先调用的是A的无参构造函数,然后依次调用B和C的构造函数,但A的构造函数只调用一次。所以输出Abcd。

7.2 计算虚继承中对象占用的空间

代码:

class A
{
    void func1(){}
};

class B : public A
{
    void func2(){}
};

class C : public virtual A
{
    void func3(){}
};

int main()
{
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    cout << sizeof(C) << endl;
}

输出 :

1
1
4
请按任意键继续. . .

分析 :类A没有任何数据成员,在内存中只有一个字节的占位符。类B也只有一个字节的占位符。类C虚继承自类A,类C中有一个指向虚基类的指针,占有4字节。

代码:

class A
{
    virtual void func1(){}
};

class B : public A
{
    void func2(){}
};

class C : public A
{
    virtual void func3(){}
};

class D : virtual public A
{
    virtual void func4() {}
};

int main()
{
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    cout << sizeof(C) << endl;
    cout << sizeof(D) << endl;
}

输出:

4
4
4
12
请按任意键继续. . .

分析: 类A中没有任何数据成员,但有虚函数存在,类A的对象中会有一个指向虚函数表的指针,因此A的占有空间为4字节。B中继承了类A,也有一个指向虚函数表的指针。类C中不但继承了类A的虚函数,而自己也有虚函数。因为两个虚函数关联一个虚函数表所以类C中也只有一个指向虚函数表的指针。类D中,对于笔者VS2015的编译器来说,类D的对象中有三个指针:指向虚基类A的指针、指向虚基类A的虚函数表的指针、指向类D的虚函数表的指针。所以占12个字节。

8. 多态与虚函数

8.0 虚函数是实现多态的手段

多态是指不同对想对于同样的消息做出不同的响应,C++中多态通过虚函数实现。

多态的原理是延迟绑定,也就是在函数调用时才绑定函数,这也是虚函数的工作原理。

一个多态示例如下:

class A
{
public:
    virtual void func1() { cout << "A : func1() " << endl; }
    void func2() { cout << "A : func2() " << endl; }
};

class B : public A
{
public:
    virtual void func1() { cout << "B : func1() " << endl; }
    void func2() { cout << "B : func2() " << endl; }
};

int main()
{
    A * a = new B;
    a->func1();
    a->func2();
}

A类中的func1被声名为虚函数,func2为普通函数。类B继承类A并覆盖了func1和func2,在主函数中定义了一个A类指针a,实际指向一个B的实例。由于func1为虚函数,根据多态原则,调用func1时会根据指针指向对象类型动态选择函数,因此 a->func1()会调用B类中的func1。而func2不是虚函数,调用函数时又会根据指针类型选择函数,因此a->func2()会调用类A中func2.

 构造函数不能声名为虚函数,但是析构函数可以声名为虚函数。静态函数不能声名为虚函数。

虚函数具有继承性,在父类中声名为虚函数的函数,在子类中即使不显示使用virtual关键字,仍然是虚函数。

8.1 虚函数表的概念

如果一个类中有虚函数,那么这个类就对应一个虚函数表,虚函数表中元素是一组指向函数的指针,每个指针指向一个虚函数入口地址。在访问函数时,通过虚函数表进行函数调用。该指向虚函数表的指针称为虚指针,虚指针位于对象模型的顶部。

如下面这段代码:

class A
{
public:
    int a;
    virtual void func1() { cout << "A : func1() " << endl; }
    virtual void func2() { cout << "A : func2() " << endl; }
};

其对象空间模型如下:

C/C++面对对象_第1张图片

8.2 纯虚函数于抽象类

所谓抽象类是指不能实例化具体对象的类,只要函数一个纯虚函数,那么该类就是纯虚函数,下面是一个应用示例:

class Shape
{
public:
    float x;
    float y;
public:
    Shape(float i,float j):x(i),y(j){}
    virtual void printArea() = 0;
};

class Rectangle : public Shape
{
public:
    Rectangle(float i, float j) : Shape(i,j){}
    void printArea() { cout << "Rectangle : " << x * y << endl; }
};

class Triangle : public Shape
{
public:
    Triangle(float i, float j) : Shape(i, j) {}
    void printArea() { cout << "Triangle : " << x * y / 2 << endl; }
};

int main()
{
    Shape * p = NULL;

    p = new Rectangle(5.0, 5.0);
    p->printArea();
    delete p;
    p = NULL;

    p = new Triangle(5.0, 5.0);
    p->printArea();
    delete p;
    p = NULL;
}

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