C++语言--面向对象--2.1--面向对象的四大主要特征、内联函数、析构函数

前言:继续C++系列基础语言。



1.面向对象的四大主要特征

抽象:面向对象的思想要求程序员将程序的每一部分都看作一个抽象的对象,即程序是由一组抽象的对象组成的,更复杂点,

这些对象根据它们相同的特征又进一步组成了一个类。


封装:


继承:不多说了。

多态:





2.内联函数

1、inline的引出

考虑下列min()函数

[cpp] view plain copy
  1. int min( int v1, int v2 )  
  2. {  
  3.     return( v1 < v2 << v1 : v2 );  
  4. }  
      为这样的小操作定义一个函数的好处是:

     a.如果一段代码包含min()的调用,那阅读这样的代码并解释其含义比读一个条件操作符的实例,可读性会强很多。

     b.改变一个局部化的实现比更改一个应用中的300个出现要容易得多

     c.语义是统一的,每个测试都能保证相同的方式实现

     d.函数可以被重用,不必为其他的应用重写代码

     不过,将min()写成函数有一个严重的缺点:调用函数比直接计算条件操作符要慢很多。那怎么能兼顾以上优点和效率呢?C++提供的解决方案为inline(内联)函数

2、inline的原理:代码替代

       在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来进行替代。

       例如,如果一个函数被指定为inline 函数则它将在程序中每个调用点上被内联地展开例如

[cpp] view plain copy
  1. int minVal2 = min( i, j );  
在编译时被展开为
[cpp] view plain copy
  1. int minVal2 = i < j << i : j;  
 则把min()写成函数的额外执行开销从而被消除了。

3、inline的使用

       让一个函数成为内联函数,隐式的为在类里定义函数,显式的则是在函数前加上inline关键字说明。

4、使用inline的一些注意事项

      a.从inline的原理,我们可以看出,inline的原理,是用空间换取时间的做法,是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。所以,如果函数体代码过长或者函数体重有循环语句,if语句或switch语句或递归时,不宜用内联

      b.关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。内联函数调用前必须声明。《高质量C/C++编程》里一个例子。

[cpp] view plain copy
  1. inline void Foo(int x, int y); // inline 仅与函数声明放在一起  
  2. void Foo(int x, int y)  
  3. {  
  4.     ...  
  5. }  
以上代码不能成为内联函数,而以下则可以

[cpp] view plain copy
  1. void Foo(int x, int y);  
  2. inline void Foo(int x, int y) // inline 与函数定义体放在一起  
  3. {  
  4.     ...  
  5. }  
       所以说,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。对于以上例子,林锐还建议,只在定义前加上inline,而不是在声明和定义前都加,因为这能体现高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈。

       c.inline对于编译器来说只是一个建议,编译器可以选择忽略该建议。换句话说,哪怕真的写成了inline,也没有任何错误的情况下,编译器会自动进行优化。所以当inline中出现了递归,循环,或过多代码时,编译器自动无视inline声明,同样作为普通函数调用。


总结下:

       楼主觉得可以将内联理解为C++中对于函数专有的宏,对于C的函数宏的一种改进。对于常量宏,C++提供const替代;而对于函数宏,C++提供的方案则是inline。在C中,大家都知道宏的优势,编译器通过复制宏代码的方式,省去了参数压栈,生成汇编的call调用,返回参数等操作,虽然存在一些安全隐患,但在效率上,还是很可取的。

       不过函数宏还是有不少缺陷的,主要有以下:

       a.在复制代码时,容易出现一想不到的边际效应,比如经典的

[cpp] view plain copy
  1. #define MAX(a, b) (a) > (b) ? (a) : (b)  
在执行语句:
[cpp] view plain copy
  1. result = MAX(i, j) + 2 ;  
时,会被解释为

[cpp] view plain copy
  1. result = (i) > (j) ? (i) : (j) + 2 ;  
     b.使用宏,无法进行调试,虽然windows提供了ASSERT宏

     c.使用宏,无法访问类的私有成员
      所以,C++ 通过内联机制,既具备宏代码的效率,又增加了安全性,还可以自由操作类的数据成员,算是一个比较完美的解决方案。



3.析构函数

 

设计一个类时,如何写析构函数? 
析构函数如果我们不写的话,C++ 会帮我们自动的合成一个,就是说:C++ 会自动的帮我们写一个析构函数。很多时候,自动生成的析构函数可以很好的工作,但是一些重要的事迹,就必须我们自己去写析构函数。 
析构函数和构造函数是一对。构造函数用于创建对象,而析构函数是用来撤销对象。简单的说:一个对象出生的时候,使用构造函数,死掉的时候,使用析构函数。

下面我们来做一个例子,看看:

#include <iostream>
#include <string>

using namespace std;

class NoName{
public:
    NoName():pstring(new std::string), i(0), d(0){}
private:
    std::string * pstring;
    int i;
    double d;
};

int main(){

    return 0;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

像上面这个 NoName 类这样的设计,类里面有一个成员变量是指针(std::string *pstring) ,那么在构造函数里我们使用 new 创建了对象,并使用 pstring 来操作这个对象。那么在这个情况下,我们就必须设计一个析构函数。

析构函数是这样编写的:(可以在类的里面声明,定义写在类的外面,)

class NoName{
public:
    NoName():pstring(new std::string)
        , i(0), d(0){
        cout << "构造函数被调用了!" << endl;
    }
    ~NoName();
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
NoName::~NoName(){
    cout << "析构函数被调用了!" << endl;
}
 
 
  • 1
  • 2
  • 3

析构函数是这样写的: ~NoName() ,它与构造函数唯一的区别就是,前面都加了一个 ~ 符号。 析构函数都是没有参数的,也就是说:析构函数永远只能写一个。 
按照 C++ 的要求,只要有 new 就要有相应的 delete 。这个 new 是在构造函数里 new 的,就是出生的时候。所以在死掉的时候,就是调用析构函数时,我们必须对指针进行 delete 操作。

我们来在main() 函数中创建一个 NoName 实例对象来 测试一下:

int main(){
    NoName a;

    return 0;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

并且在 main() 函数的 } 这一行添加一个断点,如图所示:

这里写图片描述

运行输出:

构造函数被调用了!
析构函数被调用了!
 
 
  • 1
  • 2

在定义 a 对象的时候,调用了 NoName 类的构造函数,在main() 函数执行完的时候,就是执行完 return 0;这句话后,main() 函数的执行域结束,所以就要杀掉 a 对象,所以这个时候会调用 NoName 类的析构函数。

那么,如果我们在 main() 函数中使用 new 关键字来创建一个 NoName 类的实例化对象,会出现什么样的运行效果呢? 
main() 函数中的代码如下:

int main(){
    NoName a;
    NoName *p = new NoName;

    return 0;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

运行输出:

构造函数被调用了!
构造函数被调用了!
析构函数被调用了!
 
 
  • 1
  • 2
  • 3

这里使用 new 创建的对象,就必须要使用 delete 来释放它。 牢牢的记住:new 和 delete 是一对。正确的代码是下面这个样子的:

int main(){
    NoName a;
    NoName *p = new NoName;

    delete p;
    return 0;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

运行输出:

构造函数被调用了!
构造函数被调用了!
析构函数被调用了!
析构函数被调用了!
 
 
  • 1
  • 2
  • 3
  • 4

构造函数 和 析构函数 各有各的用途,在构造函数中,我们来获取资源;在析构函数中,我们来释放资源。释放了之后,这些资源就会被回收,可以被重新利用。 
比如说,我们在构造函数里打开文件,在析构函数里关闭打开的文件。这是一个比较好的做法。 
在构造函数里,我们去连接数据库的连接,在析构函数里关闭数据库的连接。 
在构造函数里动态的分配内存,那么在析构函数里把动态分配的内存回收。

构造函数 和 析构函数 之间的操作是向对应的。 
如果我们不写析构函数,C++ 会帮我们写一个析构函数。C++帮我们写的这个析构函数只能做一些很简单的工作,它不会帮助我们去打开文件、连接数据库、分配内存这些操作,相应的回收,它也不会给我们写。所以需要我们自己手动的写。(如果要做这些操作,我们必须自己写。)

如果我们自己写了析构函数,记住三个原则: 
如果你写了析构函数,就必须同时写赋值构造函数 和 赋值操作符。你不可能只写一个。

赋值构造函数:


 
 
  • 1

在赋值的时候,不是讲指针赋值过来,而是将指针对应指向的字符串赋值过来,这是最关键的一步。

因为我们写了析构函数,就必须要将赋值构造函数写上:

class NoName{
public:
    NoName():pstring(new std::string)
        , i(0), d(0){
        cout << "构造函数被调用了!" << endl;
    }
    NoName(const NoName & other);
    ~NoName();
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
NoName::NoName(const NoName & other){
    pstring = new std::string;
    *pstring = *(other.pstring);
    i = other.i;
    d = other.d;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

除了要写 赋值构造函数,还要写赋值操作符。

class NoName{
public:
    NoName():pstring(new std::string)
        , i(0), d(0){
        cout << "构造函数被调用了!" << endl;
    }
    NoName(const NoName & other);
    ~NoName();

    NoName& operator =(const NoName &rhs);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
NoName& NoName::operator=(const NoName &rhs){
    pstring = new std::string;
    *pstring = *(other.pstring);
    i = other.i;
    d = other.d;
    return *this;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

只要你写了析构函数,就必须要写 赋值构造函数 和 赋值运算符,这就是著名的 三法则 (rule of three



总结:

在设计一个类的时候,如果我们一个构造函数都没有写,那么 C++ 会帮我们写一个构造函数。只要我们写了一个构造函数,那么 C++ 就不会再帮我们写构造函数了。

构造函数可以重载,可以写很多个,析构函数不能重载,只能写一个。如果我们没有写析构函数,C++会自动帮我们写一个析构函数。那么在工作的时候,我们写的析构函数会被调用,调用完成之后,C++会执行它自动生成的析构函数。

如果我们写的类是一个没有那么复杂的类,我们可以不需要写析构函数。如果一个类只要有这些情况:打开文件、动态分配内存、连接数据库。简单的说:就是只要构造函数里面有了 new 这个关键词,我们就需要自己手动编写析构函数。

那么如果我们写了析构函数,就必须要注意三法则:同时编写:析构函数、赋值构造函数、赋值运算符。

完整的代码:

#include <iostream>
#include <string>

using namespace std;

class NoName{
public:
    NoName():pstring(new std::string)
        , i(0), d(0){
        cout << "构造函数被调用了!" << endl;
    }
    NoName(const NoName & other);
    ~NoName();

    NoName& operator =(const NoName &rhs);
private:
    std::string * pstring;
    int i;
    double d;
};

NoName::~NoName(){
    cout << "析构函数被调用了!" << endl;
}

NoName::NoName(const NoName & other){
    pstring = new std::string;
    *pstring = *(other.pstring);
    i = other.i;
    d = other.d;
}

NoName& NoName::operator=(const NoName &rhs){
    pstring = new std::string;
    *pstring = *(rhs.pstring);
    i = rhs.i;
    d = rhs.d;
    return *this;
}

int main(){
    NoName a;
    NoName *p = new NoName;

    delete p;
    return 0;
}


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值