C++面向对象程序设计

C++面向对象编程

类与对象

定义一个类本质上是定义一个数据类型的蓝图

using namespace std;

/*
C++ 中的类&对象
*/
class Box { // define a class
public:
    double length;  // 箱子长度
    double width;  // 箱子宽度
    double height;  // 箱子高度
};


int main()
{
    // 定义C++中的对象
    Box Box1;
    // box1详述
    Box1.height = 5.0;
    Box1.length = 6.0;
    Box1.width = 7.0;

    double volume = Box1.height * Box1.width * Box1.length;
    cout << "Box1 的体积:" << volume << endl;

    return 0;
}

编译上述程序可以得到

Box1 的体积:210

类的成员函数

类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。

class Box { // define a class
public:
    double length;  // 箱子长度
    double width;  // 箱子宽度
    double height;  // 箱子高度
    // 类的成员函数
    double getVolume(void); // 返回体积 
};

double Box::getVolume(void) {
    return length * height * width;
}

类的访问修饰符

数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记public、private、protected 来指定的。关键字 public、private、protected称为访问修饰符。

公有成员 public

是指不需要类的成员函数,在类外部可以直接访问和设置的成员。

私有成员 private

私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员,不能被派生类访问 。

class Box { // define a class
public:
    double length;  // 箱子长度
    double width;  // 箱子宽度
    double height;  // 箱子高度
    // 类的成员函数
    double getVolume(void); // 返回体积 
    double setVolume(void); // 设置对象中的volume私有变量
private:
    double volume;  // 箱子体积
};

double Box::setVolume(void) {
    volume = length * height * width;
}

double Box::getVolume(void) {
    return length * height * width;
}

int main()
{
    // 定义C++中的对象
    Box Box1;
    // box1详述
    Box1.height = 5.0;
    Box1.length = 6.0;
    Box1.width = 7.0;

    Box1.setVolume();
    cout << Box1.volume << endl;
    double volume = Box1.height * Box1.width * Box1.length;
    cout << "Box1 的体积:" << volume << endl;

    return 0;
}

我们会发现cout << Box1.volume << endl;这一行会报错,因为不允许我们直接调用那个成员变量。

保护成员 protected

其和private很类似,但是不同在于,在继承的类中,protected中的变量和成员函数是可以被调用的。

// 学习CPP.cpp: 定义控制台应用程序的入口点。
//

#include "stdafx.h"
using namespace std;

class Box {
public:
    int height;
    int length;
    void setProperties(void);
protected:
    int width;
};

void Box::setProperties(void) {
    height = 5;
    width = 4;
    length = 3;
}

class SmallBox :public Box {
public:
    void printVolume(void);
};

void SmallBox::printVolume(void) {
    cout << height * width*length << endl;
}

int main() {
    SmallBox box;
    box.setProperties();
    box.printVolume();

    return 0;
}

继承Box后的SmallBox仍可以访问width

类构造函数和析构函数

构造函数

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

析构函数

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。

析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

// 学习CPP.cpp: 定义控制台应用程序的入口点。
//

#include "stdafx.h"
using namespace std;

class Box {
public:
    int height;
    int length;
    int width;
    Box(int height,int length,int width);  // 构造函数
    ~Box();  // 析构函数
};

Box::Box(int height, int length, int width) : height(height), length(length), width(width){  // 使用初始化列表来初始化字段
    cout << "构造函数" << endl;
    cout << "volume is:" << height * width*length << endl;
}

Box::~Box() {
    cout << "析构函数" << endl;
}


int main() {
    Box box(1,2,3);

    return 0;
}

输出如下:

构造函数
volume is:6
析构函数
请按任意键继续. . .

拷贝构造函数

拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其唯一的形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。

调用拷贝构造函数的情形

在C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):

  1. 一个对象作为函数参数,以值传递的方式传入函数体;

  2. 一个对象作为函数返回值,以值传递的方式从函数返回;

  3. 一个对象用于给另外一个对象进行初始化(常称为赋值初始化);

为何需要拷贝函数?

class CExample {
public:
    CExample() { pBuffer = NULL; nSize = 0; };
    ~CExample() { delete[] pBuffer; };
    void init(int n) { pBuffer = new char[n]; nSize = n; };
private:
    char *pBuffer;
    int nSize;
};

int main() {
    CExample theObj1;
    theObj1.init(40);
    // 现在需要另一个对象,并将它初始化为theObj1
    CExample theObj2 = theObj1;

    return 0;
}

我们观察上面这部分的代码,发现虽然theObj2是以theObj1的值初始化了,但是复制的仅仅是指针,指针指向的内存却还是一样的,意思就是,我要是theObj1指向的内存发生了改变,则theObj2的指向也发生了变化,这显然不是我们想要的,于是,我们便需要通过拷贝构造函数来解决这个问题。

class CExample {
public:
    CExample() { pBuffer = NULL; nSize = 0; };
    CExample(const CExample &Obj);  // 拷贝构造函数
    ~CExample() { delete[] pBuffer; };
    void init(int n) { pBuffer = new char[n]; nSize = n; };
private:
    char *pBuffer;
    int nSize;
};

CExample::CExample(const CExample &Obj) {
    nSize = Obj.nSize;
    pBuffer = new char[nSize];  // 分配内存
    memcpy(pBuffer, Obj.pBuffer, nSize*sizeof(char));
};

int main() {
    CExample theObj1;
    theObj1.init(40);
    // 现在需要另一个对象,并将它初始化为theObj1
    CExample theObj2 = theObj1;

    return 0;
}

而这样子通过拷贝构造函数,我们便可以得到完全一样,但是各自独立的对象了。

友元函数

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend,如下所示:

class Box {
private:
    int width;
    int height;
    int length;
public:
    void setProperities();
    friend void printVolume(Box box);  // 友元函数
};

void Box::setProperities() {
    width = 2;
    height = 3;
    length = 4;
}

void printVolume(Box box) {
    cout << box.width * box.height * box.length << endl;
}

int main() {
    Box box;
    box.setProperities();
    printVolume(box);
}

输出如下:24

优缺点 1、优点:能够提高效率,表达清晰、简单; 2、缺点:破坏了类的封装(尽量不要使用成员函数,除非不得已才使用友元函数)。

内联函数

C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。

对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。

如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。

在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。

Tip: 只有当函数只有 10 行甚至更少时才将其定义为内联函数.

inline int max(int a, int b) {
    return a > b ? a : b;
}

this指针

在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。

友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。

class Box {
private:
    int width;
    int height;
    int length;
public:
    int calVolume() { return width * height*length; };
    void setProperities();
    friend void printVolume(Box box);  // 友元函数
    bool compare() { return calVolume() > 20; };
};

void Box::setProperities() {
    width = 2;
    height = 3;
    length = 4;
}

void printVolume(Box box) {
    cout << box.width * box.height * box.length << endl;
}



int main() {
    Box box;
    box.setProperities();
    cout << box.compare() << endl;
}

这段代码返回结果是1;

class Box {
private:
    int width;
    int height;
    int length;
public:
    int calVolume() { return width * height*length; };
    void setProperities();
    friend void printVolume(Box box);  // 友元函数
    bool compare() { return this->calVolume() > 20; };  // 使用this
};

void Box::setProperities() {
    width = 2;
    height = 3;
    length = 4;
}

void printVolume(Box box) {
    cout << box.width * box.height * box.length << endl;
}



int main() {
    Box box;
    box.setProperities();
    cout << box.compare() << endl;
}

这段代码同样返回了1,实际上,我们在第一段代码直接调用calVolume()的时候,就是隐式的调用了this

指向类的指针

一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。

静态成员

我们可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。

静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,如下面的实例所示。

class Box {
public:
    static int count;
    Box();
};

Box::Box() {
    cout << "create a object" << endl;
    count++;
}

// 初始化静态变量
int Box::count = 0;

int main() {
    Box box1;
    Box box2;
    cout << "has " << Box::count << " objects" << endl;
}

输出如下:

create a object
create a object
has 2 objects

继承

基类和派生类

class Shape {
public:
    void setWidth(int w);
    void setHeight(int h);
protected:
    int width;
    int height;
};

void Shape::setHeight(int h) {
    height = h;
};

void Shape::setWidth(int w) {
    width = w;
}

// 派生类
class Rectangle :public Shape {
public:
    int getArea() {
        return width * height;
    };
};

int main(){
    Rectangle rec;
    rec.setHeight(4);
    rec.setWidth(5);

    // 输出对象面积
    cout << "对象面积为:" << rec.getArea() << endl;

    return 0;
}

访问控制和继承

访问 public protected private
同一个类 yes yes yes
派生类 yes yes no
外部的类 yes no no

继承类型

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。

重载运算符和重载函数

C++ 允许在同一作用域中的某个函数运算符指定多个定义,分别称为函数重载运算符重载

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。

当您调用一个重载函数重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策

函数重载

class printData {
    // 默认使用private修饰
public:
    void print(int target) {
        cout << "整数为:" << target << endl;
    };

    void print(double tar) {
        cout << "浮点数为:" << tar << endl;
    };
};

int main() {
    printData p;
    p.print(2.3);
    p.print(2);
}

编译运行结果如下:

浮点数为:2.3
整数为:2

运算符重载

您可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

class Box {
public:
    int getVolume() {
        return height * weight*length;
    };
    Box() {
        // init
        height = 5;
        weight = 4;
        length = 3;
    };

    Box operator+(const Box &b) {
        Box box;
        box.length = length + b.length;
        box.weight = weight + b.weight;
        box.height = height + b.height;
        return box;
    }
private:
    int height;
    int weight;
    int length;
};

int main() {
    Box box1;
    Box box2;
    Box box3;


    box3 = box1 + box2;

    cout << "Box3 的体积为:" << box3.getVolume() << endl;
    return 0;
}

输出为:

Box3 的体积为:480

多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

class Shape {
protected:
    int width;
    int height;
public:
    Shape(int a = 0, int b = 0) : width(a), height(b) {};
    int area() {
        cout << "Parent Class area" << endl;
        return 0;
    }
};

class Rectangle : public Shape {
public:
    Rectangle(int a=0, int b=0) :Shape(a, b) {};
    int area() {
        cout << "Rectangle Class area" << endl;
        return 0;
    }
};

int main() {
    Shape *shape;
    Rectangle rec(10, 7);

    // 存储矩形的地址
    shape = &rec;
    shape->area();
    return 0;
}

运行结果如下:

Parent Class area

导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。

class Shape {
protected:
    int width;
    int height;
public:
    Shape(int a = 0, int b = 0) : width(a), height(b) {};
    virtual int area() {  // 增加了virtual
        cout << "Parent Class area" << endl;
        return 0;
    }
};

class Rectangle : public Shape {
public:
    Rectangle(int a=0, int b=0) :Shape(a, b) {};
    int area() {
        cout << "Rectangle Class area" << endl;
        return 0;
    }
};

int main() {
    Shape *shape;
    Rectangle rec(10, 7);

    // 存储矩形的地址
    shape = &rec;
    shape->area();
    return 0;
}

这段代码的输出为:

Rectangle Class area

而我们得到了想要的结果,我们做的仅仅是将int area()前面加上了virtual将静态链接改为了动态链接。

此时,编译器看的是指针的内容,而不是它的类型。因此,由于 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

虚函数

虚函数 是在基类中使用关键字virtual声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

纯虚函数

您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

我们可以把基类中的虚函数area() 改写如下:

class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      // pure virtual function
      virtual int area() = 0;
};

= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 今天老师讲的是食字,作业是写一件关于吃饭的难忘的事,我想了想,要说关于吃饭的难忘的事,就是我去年的那个生日...
    刘泽宇4联西阅读 313评论 3 2