C/C++心得面向对象

      首先本文以C++描述面向对象。面向对象应该可以说是C++对C最为重要的扩充。面向对象使得C++可以用更符合人的思维模式的方式编程,使得有一定基础的程序员可以更容易的写程序。相对于C,C++还有其他许多方面的改进,唯一的缺点就是损失了些许效率。本文只针对笔者心中的C++面向对象进行总结说明。

      最开始学习面向对象切忌浮躁,需要有耐心的去理解。如果不能从文字解释上理解这里面“对象”这个词,可以从C++增加的关键字class的用法开始理解,这个后面再进行说明。

1.C与C++对比

  在C++编译环境中完全使用C的语法是可以编译通过的,除了一些原本不太规范的地方,比如:无返回类型的函数、非void返回类型函数内部无实际返回值等等在C++编译环境中无法编译通过。

// 无返回类型
test1()
{

}

int test2()
{
    // 无返回语句
}

  相对于C,C++最重要的改动就是面向对象,面向对象一词算是对大部分改动的一个概括,下面以“给定直角三角形的两条直角边长,求该三角形的面积”这个简单问题,用C/C++写出几种不同的解法

      (对于几十行的小程序来说,从哪里开始读代码都无所谓,但是大程序,我相信多数人都是从main函数开始读代码的,这样来确定代码的业务流程)

      首先用C语言,直接输出,这种方式代码量少,但可读性很差,如果没有文字说明这是求直角三角形面积,不好确定代码究竟是干什么的

/*
 * 直接输出的方式,这种方式可读性很差,
 * 如果没有文字说明这是求直角三角形面积,不好确定代码究竟是干什么的。
*/
#include <stdio.h>

int main(int arg, char * args[])
{
    // 直接输出
    printf("The area is :%.0f\n", 3.0f * 4.0f / 2);
    getchar();
    return 0;
}

  使用C语言结构体和函数相结合的方式,代码量提升,但是可以很清晰的知道代码的意图。(有个前提,英语要稍微合格,毕竟大多数程序语言都是英美出品,如果英语不合格,你就不知道RightTriangle是直角三角形的意思)

 1 #include <stdio.h>
 2 // 直角三角形结构体
 3 struct RightTriangle
 4 {
 5     float side1;
 6     float side2;
 7 };
 8 // 获取一个直角三角形面积
 9 float getArea(struct RightTriangle triangle)
10 {
11     return triangle.side1 * triangle.side2 / 2;
12 }
13 
14 int main(int arg, char * args[])
15 {
16     struct RightTriangle triangle = { 3, 4 };
17     printf("The area is : %.0f\n", getArea(triangle));
18     getchar();
19     return 0;
20 }

  下面算是很接近C语言写法的C++写法实现(对C++来说不是很规范),可以发现C++中函数可以写在结构体内部。std::cout是C++中标准打印方式,相当于C语言中的printf,不过需要用到“<<”操作符,std::cout的好处是可以自动匹配值类型。

  多个输出项都可以用“<<”操作符连接起来,std::endl是换行的意思。

  从main函数中的语句可以清晰读出获取两边长为3,4的直角三角形的面积。

 1 #include <iostream>
 2 // 这里先用结构体作为示例,相对于C语言的struct,可以在struct内部使用同名函数对内部变量进行初始化
 3 // 这个函数叫做构造函数,还可以在struct写其他相关计算的函数,如函数getArea
 4 struct CRightTriangle
 5 {
 6     CRightTriangle(float side1, float side2)
 7     {
 8         m_fside1 = side1;
 9         m_fside2 = side2;
10     }
11     float getArea()
12     {
13         return m_fside1 * m_fside2 / 2;
14     }
15     float m_fside1;
16     float m_fside2;
17 
18 };
19 
20 int main(int arg, char * args[])
21 {
22     // 输出直角边分别为3,4的直角三角形的面积
23     std::cout << "The area is : "<< CRightTriangle(3, 4).getArea() << std::endl;
24 
25     getchar();
26     return 0;
27 }

  如果整个程序真的可以不到10行的话,个人建议还是第一种写法,但是我想不会有谁学C/C++就为了自己个人写个几十行代码的程序的。大部分的开发都是许多人通力合作的,所以程序的可读性很重要。

2.封装

  封装指的是隐藏细节,使得代码模块化。上面代码中getArea()可以算是代码模块话,那么隐藏细节又指什么呢?这里需要改造下上面的代码进行说明:

 1 #include <iostream>
 2 
 3 class CRightTriangle
 4 {
 5 public:
 6     // 构造函数,用来初始化对象,初始化操作包括设置对象内部初始值
 7     CRightTriangle(float side1, float side2) :m_fside1(side1), m_fside2(side2) // 使用初始化列表初始化值
 8     {
 9     }
10     float getArea()
11     {
12         return m_fside1 * m_fside2 / 2;
13     }
14 private:
15     float m_fside1;
16     float m_fside2;
17 
18 };
19 
20 int main(int arg, char * args[])
21 {
22     // 输出直角边分别为3,4的直角三角形的面积
23     CRightTriangle triangle(3, 4);
24     std::cout << "The area is : " << triangle.getArea() << std::endl;
25 
26     getchar();
27     return 0;
28 }

  C++中,更倾向于使用class,也就是类,类定义出的变量叫做对象。

  相比较于上一节的代码,首先我将struct换成了class,C++中struct和class的区别很小,主要是默认访问权限的区别。c++中struct的默认访问权限是public,class的默认访问权限是private。访问权限暂且不细说,暂时要知道,private的访问权限的内容无法通过"."操作符调用,比如上面的代码,定义triangle后,无法使用triangle.m_fside1获取该值,而在上一节的代码中是可以这样获取的。这就叫做隐藏细节

  访问说明符有private,protected,public三种(protected在下节继承中在细说),除了使用struct和class控制默认访问权限外,还可以用在代码中写访问说明符的方式规定访问权限,显示访问说明符的作用范围是直到遇到下一个访问说明符或者到类的末尾。

  隐藏细节,使得代码模块化,这就是所谓封装。

3.继承

  继承是两个类之间的一种关系(硬要说也可以是多个类之间的一种关系,这里以两个类来说明)。

  类B公共继承于类A,那么类A是类B的父类,类B是类A的子类(或派生类)。类A中所有的公共成员函数和变量,那么在类B中无需任何代码可以直接使用。

 1 #include <iostream>
 2 
 3 class A
 4 {
 5 public:
 6     // 多个同名函数的代码现象被称为函数重载,只要同名函数相互间的形参个数或者形参类型不同,就不会编译出错
 7     // 构造函数重载
 8     A(int a, int b) :m_na(a), m_nb(b){ std::cout << "A Con" << std::endl; }
 9     // 符号~加上类名的函数叫做析构函数,会在对象被释放时自动调用
10     // 一般用于释放用户申请的空间
11     ~A(){ std::cout << "A De" << std::endl; }
12     // 可以以下面这种方式代理初始化
13     A() :A(0, 0){}
14     int getSum(){ return m_na + m_nb; }
15     int m_na;
16     int m_nb;
17     
18 };
19 // 类B public继承类A
20 class B :public A
21 {
22 public:
23     // 类A没有默认无参构造函数,所以这样
24     B(int a, int b, int c) :A(a, b), m_nc(c)
25     { std::cout << "B Con" << std::endl; }
26     ~B(){ std::cout << "B De" << std::endl; }
27 
28     // B可直接使用A的公共变量
29     int getProduct()
30     {
31         return m_na * m_nb * m_nc;
32     }
33 
34     B() :A(0, 0){}
35     // B类自己的成员
36     int m_nc;
37 };
38 
39 
40 int main(int arg, int args[])
41 {
42     // 加上大括号可以观察对象构造和析构的顺序
43     {
44         B b(5, 4, 3);
45         // B可直接使用A的函数和变量
46         std::cout << b.getSum() << std::endl;
47         // B自己的成员函数
48         std::cout << b.getProduct() << std::endl;
49         // 因为是公共成员所以可以直接访问
50         std::cout << b.m_na + b.m_nb + b.m_nc;
51         getchar();
52     }
53     getchar();
54     return 0;
55 }

 (继承说明代码)

以上代码能够还算清楚的展示继承的用法,其中,类A和类B的所有成员都是公共的(public),类B也是公共继承于类A,所以可以访问A中的成员。下面以继承说明代码称呼这段程序。

将类A中的成员变量变为私有的(private):

 1 class A
 2 {
 3 public:
 4     A(int a, int b) :m_na(a), m_nb(b){ std::cout << "A Con" << std::endl; }
 5     ~A(){ std::cout << "A De" << std::endl; }
 6     // 可以以下面这种方式代理初始化
 7     A() :A(0, 0){}
 8     int getSum(){ return m_na + m_nb; }
 9 private:
10     int m_na;
11     int m_nb;
12 };

这个时候继承说明代码中的31行和50行会报错,因为私有成员无法继承。但是B类定义的对象还是可以使用getSum函数。

将类A中的成员变量变为受保护的(protected):

这个时候继承说明代码中的50行会报错,因为受保护成员可以继承,但是无法外部访问。但是B类定义的对象还是可以使用getSum函数。

下面说说继承方式,以上所有的测试代码都是在类B共有继承类A的情况。类的继承同样分为三种,公有继承(public),私有继承(private),保护继承(protected)。三种继承模式的共同点是,原来私有的,继承后就不可见(不允许子类使用)。不同点就是,私有继承会将其他所有父类成员改为私有的,受保护继承会将其他所有父类成员改为受保护的,共有继承会保留其他所有父类成员的访问权限。

比如上面把类B改为私有继承或者保护继承类A,那么继承说明代码中第53行会报错。

继承暂时说这么多。

4.多态

应该说正是有了多态才使得面向对象变得伟大,多态使得许多设计模式得以实现。

多态概念比较抽象,较为普通的解释就是同样的调用语句有多种不同的执行效果。

多态发生在有继承关系的类之间,要体现多态关系至少得有三个类,且其中两个类继承于另一个类。且这两个子类需要实现父类中带virtual关键字的方法。最后需要一个指向父类对象的指针或者引用来操作实现多态。

类比C语言,C++中的多态有点像C中的函数指针。

下面写个简单多态的例子:

 1 #include <iostream>
 2 
 3 // 父类
 4 class Parent
 5 {
 6 public:
 7     // 只有使用virtual才能实现多态
 8     virtual void func()
 9     {
10         std::cout << "Parent" << std::endl;
11     }
12 };
13 // 子类1
14 class Child1 : public Parent
15 {
16 public:
17     virtual void func()
18     {
19         std::cout << "Child1" << std::endl;
20     }
21 };
22 // 子类2
23 class Child2 : public Parent
24 {
25 public:
26     // 如果子类不会再有子类的话,可以不用virtual
27     void func()
28     {
29         std::cout << "Child2" << std::endl;
30     }
31 };
32 // 多态的引用函数展示
33 void func(Parent & p)
34 {
35     p.func();
36 }
37 // 测试用例
38 void test()
39 {
40     // 分别定义子类
41     Parent p;
42     Child1 c1;
43     Child2 c2;
44     // 定义指向父类的指针
45     Parent *pP = &p;
46     // 下面演示指针的执行结果
47     pP->func();
48     pP = &c1;
49     pP->func();
50     pP = &c2;
51     pP->func();
52     std::cout << std::endl;
53     // 引用函数
54     func(p);
55     func(c1);
56     func(c2);
57 }
58 
59 int main(int arg, char * args[])
60 {
61     test();
62     getchar();
63     return 0;
64 }

以上代码的执行结果为:

Parent
Child1
Child2

Parent
Child1
Child2

而如果不使用virtual关键字,上面的所有结果都会是Parent。

C++中的多态的实现是当类中声明虚函数时(有virtual关键字),编译器会在类中生成一个虚函数表,该表是一个存储类成员函数指针的数据结果,由编译器检测生成维护,编译器会识别所有的带virtual的成员函数,将其放入虚函数表中。为了连接虚函数表,对于有虚函数表的每个对象中都会有个指向虚函数表的指针,一般称其为vptr指针。

我们可以用sizeof证明该指针的存在:

 1 #include <iostream>
 2 
 3 // C1是有虚函数的类
 4 class C1
 5 {
 6 public:
 7     C1() :m_na(0){}
 8     virtual void func();
 9 private:
10     int m_na;
11 };
12 // C2除了无virtual,其他和C1一样
13 class C2
14 {
15 public:
16     C2() :m_na(0){}
17     void func();
18 private:
19     int m_na;
20 };
21 
22 int main(int arg, char * args[])
23 {
24     // 输出两个类对象的占用空间大小
25     std::cout << "sizeof C1:" << sizeof(C1) << "," << "sizeof C2:" << sizeof(C2) << std::endl;
26     getchar();
27     return 0;
28 }

在32位编译环境下,执行结果为:

sizeof C1:8,sizeof C2:4

可见C1多了一个指针大小的空间,证明多了一个指针的存在。

5.简要概括

本文只是大概总结了下C++面向对象的特性以及和C语言做了下对比,许多基础的特性,如命名空间、函数重载、运算符重载、友元等方面的东西并未说明,高级应用如标准库、模板、stl等技术也未提及,如果想要打一个良好的基础,建议通读C++ Primer这本书,现在最新版本是实现了C++ 11 特性的第五版。

原文地址:https://www.cnblogs.com/yaoh/p/4458177.html