[笔试面试题] 8-面向对象篇

面向对象篇

1 面向对象与面向过程的含义以及区别?

面向对象

面向对象是把数据及对数据的操作方法放在一起,作为一个相互依存的整体,即对象。对同类对象抽象出其共性,即类,类中的大多数数据,只能被本类的方法进行处理。类通过一些简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。程序流程由用户在使用中决定。例如,站在抽象的角度,人类具有身高、体重、年龄、血型等一些特性。人类仅仅只是一个抽象的概念,它是不存在的实体,但是所有具备人类这个群体的属性与方法的对象都叫人,这个对象人是实际存在的实体,每个人都是人这个类的一个对象。

面向过程

面向过程是一种以事件为中心的开发方法,就是自顶向下顺序执行,逐步求精,其程序结构是按功能划分为若干个基本模块,这些模块形成一个树状结构,各模块之间的关系也比较简单,在功能上相对独立,每一模块内部一般都是由顺序、选择和循环三种基本结构组成的,其模块化实现的具体方法是使用子程序,而程序流程在写程序时就已经决定。例如五子棋,面向过程的设计思路就是首先分析问题的步骤:第一步,开始游戏;第二步,黑子先走;第三步,绘制画面;第四步, 判断输赢;第五步,轮到白子;第六步,绘制画面;第七步,判断输赢;第八步,返回步骤2;第九步,输出最后结果。把上面每个步骤用分别的函数来实现,就是一个面向过程的开发方法。

区别

(1) 出发点不同。

面向对象是用符合常规思维方式来处理客观世界的问题,强调把问题的要领直接映射到对象及对象之间的接口上。而面向过程方法则不然,它强调的是过程的抽象化与模块化,它是以过程为中心构造或处理客观世界问题的。

(2) 层次逻辑关系不同。

面向对象方法则是用计算机逻辑来模拟客观世界中的物理存在,以对象的集合类作为处理问题的基本单位,用类的层次结构来体现类之间的继承和发展。而面向过程方法处理问题的基本单位是能清晰准确地表达过程的模块,用模块的层次结构概括模块或模块间的关系与功能,把客观世界的问题抽象成计算机可以处理的过程。

(3) 数据处理方式与控制程序方式不同。

面向对象方法将数据与对应的代码封装成一个整体,原则上其他对象不能直接修改其数据,即对象的修改只能由自身的成员函数完成。控制程序方式上是通过“事件驱动”来激活和运行程序。而面向过程方法是直接通过程序来处理数据,处理完毕后即可显示处理结果。在控制程序方式上是按照设计调用或返回程序,不能自由导航,各模块之间存在着控制与被控制、 调用与被调用的关系。

(4) 分析设计与编码转换方式不同。

面向对象方法贯穿软件生命周期的分析、设计及编码之间,是一种平滑过程,从分析到设计再到编码采用一致性的模型表示,即实现的是一种无缝连接。而面向过程方法强调分析、设计及编码之间按规则进行转换,贯穿软件生命周期的分析、设计及编码之间,实现的是一种有缝的连接。


2 面向对象的基本特征有哪些?

面向对象方法首先对需求进行合理分层,然后构建相对独立的业务模块,最后通过整合各模块,达到高内聚、低耦合的效果,从而满足客户要求。具体而言,它有3个基本特征:封装、继承和多态。

(1) 封装是指将客观事物抽象成类,每个类对自身的数据和方法实行保护。类可以把自己的数据和方法只让可信的类或对象操作,对不可信的进行隐藏。C++中类是一种封装手段,采用类来描述客观事物的过程就是封装,本质上是对客观事物的抽象。

(2) 继承可以使用现有类的所有功能,而不需要重新编写原来的类,它的目的是为了进行代码复用和支持多态。它一般有3种形式:实现继承、可视继承、接口继承。其中,实现继承 是指使用基类的属性和方法而无需额外编码的能力;可视继承是指子窗体使用父窗体的外观和实现代码;接口继承仅使用属性和方法,实现滞后到子类实现。前两种(类继承)和后一种 (对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。

(3) 多态是指同一个实体同时具有多种形式,它主要体现在类的继承体系中,它是将父对象设置成为和一个或更多的它的子对象相等的技术,赋值以后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单地说,就是允许将子类类型的指针赋值给父类类型的指针。编译时多态是静态多态,在编译时就可以确定对象使用的形式。


3 什么是深拷贝?什么是浅拷贝?

如果一个类拥有资源(堆或者是其他系统资源),当这个类的对象发生复制过程时,资源重新分配,这个过程就是深拷贝;反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。

例如,在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位复制,也就是把对象里的值完全复制给另一个对象,如A=B,这时,如果类B中有一个成员变量指针已经申请了内存,那么类A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放 了,如通过析构函数,这时A内的指针就变成野指针了,导致运行错误。

深复制的程序示例如下:

#include <iostream> 

using namespace std; 

class CA 
{
public:
    CA(int b,char* cstr);
    CA(const CA& C); 
    void Show();
    〜CA(); 
private: 
    int a; 
    char *str;
};

CA::CA(int b,char* cstr)
{
    a=b;
    str=new char[b]; 
    strcpy(str,cstr);
}

CA::CA(const CA& C) 
{
    a=C.a;
    str=new char[a]; //给str重新分配内存空间,所以为深拷贝 
    if(str!=0)
        strcpy(str,C.str);
}

void CA::Show()
{
    cout<<str<<endl;
}

CA::〜CA()
{
    delete str;
}

int main()
{
    CA A(10,"Hello"); 
    CA B=A; 
    B.Show(); 
    
    return 0;
}

程序输出结果:

Hello

如果没有自定义复制构造函数时, 系统将会提供给一个默认的复制构造函数来完成这个过程,就会造成“浅拷贝”。所以要自定义赋值构造函数,重新分配内存资源,实现“深拷贝”。


4 什么是友元?

类具有封装、继承、多态、信息隐藏的特性,只有类的成员函数才可以访问类的标记为 private的私有成员,非成员函数可以访问类中的公有成员,但是却无法访问私有成员,为了使非成员函数可以访问类的成员,唯一的做法就是将成员都定义为public,但如果将数据成员都定义为公有的,这又破坏了信息隐藏的特性。友元正好解决了这一棘手的问题。在使用友元函数时,一般需要注意以下几个方面的问题:

(1) 必须在类的说明中说明友元函数,说明时以关键字friend开头,后跟友元函数的函数原型,友元函数的说明可以出现在类的任何地方,包括private和public部分。

(2) 友元函数不是类的成员函数,所以友元函数的实现与普通函数一样,在实现时不用 “::”指示属于哪个类,只有成员函数才使用“::”作用域符号。

(3) 友元函数不能直接访问类的成员,只能访问对象成员。

(4) 调用友元函数时,在实际参数中需要指出要访问的对象。

(6) 类与类之间的友元关系不能继承。

友元一般定义在类的外部,但它需要在类体内进行说明,为了与该类的成员函数加以区别,在说明时前面加以关键字friend。需要注意的是,友元函数不是成员函数,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率,但是它破坏了类的封装性和隐藏 性,使得非成员函数可以访问类的私有成员。

如下为一个友元函数的例子:

#include <iostream>
#include <string> 
using namespace std; 

class Fruit
{
public:
    Fruit(const string &nst="apple",const string &cst="green"):name(nst),colour(cst)
    {        
    }
    
    〜Fruit()
    {
    }
    friend istream& operator>>(istream&,Fruit&); 
    friend ostream& operator<<(ostream&,const Fruit&); 
    
    void print()
    {
        cout<<colour<<" "<<name<<endl;
    }

private:
    string name; 
    string colour;
};

ostream& operator<<(ostream &out,const Fruit &s) //重载输出操作符
{
    out<<s.colour<<" "<<s.name; 
    return out;
}

istream& operator>>(istream& in,Fruit &s) //重载输入操作符
{
    in>>s.co1our>>s.name; 
    
    if(!in)
        cerr<<"Wrong input!"<<endl; 
        
    return in;
}

int main()
{
    Fruit apple; 
    cin>>apple; 
    cout<<apple; 
    
    return 0;
}


5 类的成员变量的初始化顺序是按照声明顺序吗?

在C++中,类的成员变量的初始化顺序只与变量在类中的声明顺序有关,与在构造函数中的初始化列表的顺序无关。而且静态成员变量先于实例变量,父类成员变量先于子类成员变量。

示例程序如下:

class Test 
{
private :
    int nl; 
    int n2;     
public:
    Test();
};

Test::Test():n2(2),nl(1)
{}

当查看相关汇编代码时,就能看到正确的初始化顺序了。因为成员变量的初始化次序跟变量在内存中的次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。

从全局看,变量的初始化顺序如下:

(1) 基类的静态变量或全局变量。

(2) 派生类的静态变量或全局变量。

(3) 基类的成员变量。

(4) 派生类的成员变量。


6 一个类为另一个类的成员变量时,如何对其进行初始化?

示例程序如下:

class ABC 
{
public:
    ABC(int x, int y, int z); 
private : 
    int a; 
    int b; 
    int c;
};

class MyClass 
{
public:
    MyClass():abc(1,2,3)
    {
        
    } 

private:
ABC abc;
};

上例中,因为ABC有了显式的带参数的构造函数,那么它是无法依靠编译器生成无参构造函数的,所以必须使用初始化列表:abc(1,2,3),才能构造ABC的对象。


7 C++中的空类默认产生哪些成员函数?

C++中空类默认会产生以下6个函数:默认构造函数、复制构造函数、析构函数、赋值运算符重载函数、取址运算法重载函数、const取址运算符重载函数等。

class Empty
{
public:
    Empty();//默认构造函数
    Empty( const Empty& );//复制构造函数
    〜Empty();//析构函数
    Empty& operator=(const Empty&);// 赋值运算符重载函数
    Empty* operator&();// 取址运算重载函数
    const Empty* operator&( ) const; // const取址运算符重载函数 
};


8 C++提供默认参数的函数吗?

C++可以给函数定义默认参数值。在函数调用时没有指定与形参相对应的实参时,就自动使用默认参数。

默认参数的语法与使用:

(1) 在函数声明或定义时,直接对参数赋值,这就是默认参数。

(2) 在函数调用时,省略部分或全部参数。这时可以用默认参数来代替。

通常调用函数时,要为函数的每个参数给定对应的实参。例如:

void delay(int loops=1000);//函数声明 

void delay(int loops) //函数定义 
{
    if(loops==0)
    {
        return;
    }
    for(int i=0;i<loops;i++)
        ;
}

在上例中,如果将delay()函数中的loops定义成默认值1000,这样,以后无论何时调用delay()函数,都不用给loops赋值,程序都会自动将它当做值 1000进行处理。例如,当执行delay(2500)调用时,loops的参数值为显性化的,被设置为 2500;当执行delay()时,loops将采用默认值1000。

 

默认参数在函数声明中提供,当有声明又有定义时,定义中不允许默认参数。如果函数只有定义,则默认参数才可出现在函数定义中。例如:

oid point(int=3,int=4);//声明中给出默认值 

void point(int x,int y) //定义中不允许再给出默认值 
{
    cout<<x<<endl;
    cout<<y<<endl;
}

 

如果一组重载函数(可能带有默认参数)都允许相同实参个数的调用,将会引起调用的二义性。例如:

void func(int);//重载函数之一 
void func(int,int=4);//重载函数之二,带有默认参数 
void func(int=3,int=4);//重载函数三,带有默认参数 
func(7);//错误:到底调用3个重载函数中的哪个? 
func(20,30);//错误:到底调用后面两个重载函数的哪个?


9 什么是虚函数及其作用?

指向基类的指针在操作它的多态类对象时,可以根据指向的不同类对象调用其相应的函数,这个函数就是虚函数。

虚函数的作用:在基类定义了虚函数后,可以在派生类中对虚函数进行重新定义,并且可以通过基类指针或引用,在程序的运行阶段动态地选择调用基类和不同派生类中的同名函数。(如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。)

下面是一个虚函数的实例程序:

#include "stdafx.h"
#include<iostream> 
using namespace std;

class Base
{
public:
    virtual void Print()//父类虚函数
    {
        printf("This is Class Base!\n");
    }
};

class Derived1 :public Base
{
public:
    void Print()//子类1虚函数
    {
        printf("This is Class Derived1!\n");
    }
};

class Derived2 :public Base
{
public:
    void Print()//子类2虚函数
    {
        printf("This is Class Derived2!\n");
    }
};

int main()
{
    Base Cbase;
    Derived1 Cderived1;
    Derived2 Cderived2;
    Cbase.Print();
    Cderived1.Print();
    Cderived2.Print();

    cout << "---------------" << endl;
    Base *p1 = &Cbase;
    Base *p2 = &Cderived1;
    Base *p3 = &Cderived2;
    p1->Print();
    p2->Print();
    p3->Print();
}

/*
输出结果:

This is Class Base!
This is Class Derived1!
This is Class Derived2!
---------------
This is Class Base!
This is Class Derived1!
This is Class Derived2!
*/

需要注意的是,虚函数虽然非常好用,但是在使用虚函数时,并非所有的函数都需要定义成虚函数,因为实现虚函数是有代价的。在使用虚函数时,需要注意以下几个方面的内容:

(1) 只需要在声明函数的类体中使用关键字virtual将函数声明为虚函数,而定义函数时不需要使用关键字virtual。

(2) 当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。

(3) 非类的成员函数不能定义为虚函数,全局函数以及类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。

(4) 基类的析构函数应该定义为虚函数,否则会造成内存泄漏。基类析构函数未声明virtual,基类指针指向派生类时,delete指针不调用派生类析构函数。有 virtual,则先调用派生类析构再调用基类析构。


10 C++中如何阻止一个类被实例化?

C++中可以通过使用抽象类,或者将构造函数声明为private阻止一个类被实例化。抽象类之所以不能被实例化,是因为抽象类不能代表一类具体的事物,它是对多种具有相似性的具体事物的共同特征的一种抽象。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但是动物本身生成对象不合情理。


11 多态类中的虚函数表是 Compile-Time,还是 Run-Time 时建立的?

虚拟函数表是在编译期就建立了,各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组.而对象的隐藏成员--虚拟函数表指针是在运行期--也就是构造函数被调用时进行初始化的,这是实现多态的关键。


12 多态的作用?

主要是两个:

  1. 隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用;
  2. 接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。


13 C++函数中那些不可以被声明为虚函数 ?

常见的不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。

1.为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。

2.为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论)

3.为什么C++不支持内联成员函数为虚函数?
其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)

4.为什么C++不支持静态成员函数为虚函数?
这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。

5.为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。


14 如何修改类和排序函数,根据age对传入的类数组进行排序?

类使用大于运算符(>)重载,排序函数定义为模板函数,如下所示。

#include <iostream>
#include <string>

using namespace std;

class Student
{
public:
    Student()
    {

    }

    void SetStu(int t_len, string t_name)
    {
        age = t_len;
        name = t_name;
    }

    void Print()
    {
        cout << "age:" << age << "  name:" << name << endl;
    }

    bool operator >(const Student &another)
    {
        if (age > another.age)
            return true;
        else
            return false;
    }

private:
    int age;
    string name;
};

template <typename T>
//冒泡排序法 - 升序
void BubbleSort(T arr[], int len)
{
    T temp;
    int i, j;
    for (i = 0; i < len-1; i++)
    {
        for (j = 0; j < len-i-1; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                temp = arr[j + 1];
                arr[j + 1] = arr[j];
                arr[j] = temp;
            }
        }
    }
}

// 程序的主函数
int main()
{
    Student arr[5];
    arr[0].SetStu(2, "zhangSan");
    arr[1].SetStu(1, "liSi");
    arr[2].SetStu(3, "wangWu");
    arr[3].SetStu(5, "xiaoMing");
    arr[4].SetStu(4, "xiaoHong");

    BubbleSort(arr, 5);
    
    for (int i = 0; i < 5; i++)
        arr[i].Print();

    return 0;
}

/*
输出结果:

age:1  name:liSi
age:2  name:zhangSan
age:3  name:wangWu
age:4  name:xiaoHong
age:5  name:xiaoMing
*/



一些需要注意的要点:

  • 基类的构造函数/析构函数都不能被派生类继承。
  • 《C++程序设计语言》,将多态分为两类,虚函数提供的东西称作运行时多态,模板提供的为编译时多态和参数式多态。
  • 访问属性为private的基类成员,则不能被派生类继承。
  • C++中的结构体也必须使用new 创建。
  • C++中结构体可以定义成员函数,也可以继承,另外类也可以由结构继承而来。

转载于:https://www.cnblogs.com/linuxAndMcu/p/10197142.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值