[C++基础] 面向对象、C 与 C++ 区别篇

[C++基础] 面向对象、C 与 C++ 区别篇

 

一、面向对象

1 面向对象与面向过程的含义?

1、面向对象

面向对象是把数据及对数据的操作方法放在一起,作为一个相互依存的整体,即对象。对同类对象抽象出其共性,即类,类中的大多数数据,只能被本类的方法进行处理。类通过一些简单的外部接口与外界发生关系,对象与对象之间通过消息进行通信。

2、面向过程

面向过程是一种以事件为中心的开发方法,就是自顶向下顺序执行,逐步求精,其程序结构是按功能划分为若干个基本模块,这些模块形成一个树状结构,各模块之间的关系也比较简单,在功能上相对独立,每一模块内部一般都是由顺序、选择和循环三种基本结构组成的,其模块化实现的具体方法是使用子程序,而程序流程在写程序时就已经决定。


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

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

(1)封装是指将客观事物抽象成类,每个类对自身的数据和方法实行保护。类可以把自己的数据和方法只让可信的类或对象操作,对不可信的进行隐藏。

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

(3)多态是指同一个实体同时具有多种形式,它主要体现在类的继承体系中,简单地说,就是允许将子类类型的指针赋值给父类类型的指针,然后父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。


3 C++ 多态的实现及原理?

C++ 的多态,大概可分为函数重载和虚函数两类。

先说说函数重载,比较简单,就是函数参数的类型和个数不同罢了,返回值的类型不做讨论。函数重载是建立在 name mangling(名称改写)上的,就是说在编译的时候,编译器会为函数生成独一无二的名字,以区别重载的函数。


什么是多态性?

多态意指 “一个接口,多种实现”。一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数,运行时绑定(动态绑定)。(基类虚函数->子类覆盖->基类指针指向子类对象)


原理?

编译器为每个含有虚函数的类维护有一个虚函数表,而每个对象拥有一个虚指针(首地址保存),指向虚函数表,对象间共有虚表(vtable)。

虚表可继承,子类继承基类虚表后,虚表与父类虚表完全相同(地址不同),只是对象的虚指针指向了本类的虚表。

Base *b = new Derived();
b->foo();
  • 第一句:构造子类对象时,遇到虚函数,先不绑定(使用虚指针指向本类虚表);
  • 第二句:调用虚函数时,对象就按照虚指针所指寻找要调用函数。

静态多态和动态多态

静态多态是指通过模板技术或者函数重载技术实现的多态,其在编译器确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。


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

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

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


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

#include<iostream> 
using namespace std;

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

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

int main()
{
	Base Cbase;
	Derived Cderived;

	Base *p1 = &Cbase;
	Base *p2 = &Cderived;
	p1->Print(); // 输出:This is Class Base!
	p2->Print(); // 输出:This is Class Derived!
}

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

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

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

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

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


5 C++中为什么要将析构函数定义成虚函数?

构造函数不可以是虚函数的,这个很显然,毕竟虚函数都对应一个虚函数表,虚函数表是存在对象内存空间的,如果构造函数是虚的,就需要一个虚函数表来调用,但是类还没实例化没有内存空间就没有虚函数表,这根本就是个死循环。

可是析构函数却要定义成虚函数,这是为什么呢?

  • 其实这个很好理解,派生类的成员由两部分组成,一部分是从基类那里继承而来,一部分是自己定义的。那么在实例化对象的时候,首先利用基类构造函数去初始化从基类继承而来的成员,再用派生类构造函数初始化自己定义的部分。

  • 同时,不止构造函数派生类只负责自己的那部分,析构函数也是,所以派生类的析构函数会只析构自己的那部分,这时候如果基类的析构函数不是虚函数,则不能调用基类的析构函数析构从基类继承来的那部分成员,所以就会出现只删一半的现象,造成内存泄漏。


6 为什么不要在构造函数和析构函数中调用虚函数?

1. 不要在构造函数中调用虚函数的原因

第一个原因,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数,那么此时派生类的构造函数并未执行,所调用的函数可能操作还没有被初始化的成员,这会导致灾难的发生。

第二个原因,即使想在构造函数中实现动态联编,在实现上也会遇到困难。这涉及到对象虚指针的建立问题。在 Visual C++ 中,包含虚函数的类对象的虚指针被安排在对象的起始地址处,并且虚函数表的地址是由构造函数写入虚指针的。所以,一个类的构造函数在执行时,并不能保证该函数所能访问到的虚指针就是当前被构造对象最后所拥有的虚指针,因为后面派生类的构造函数会对当前被构造对象的虚指针进行重写,因此无法完成动态联编。

2.不要在析构函数中调用虚函数的原因

同样的,在析构函数中调用虚函数,函数的入口地址也是在编译时静态决定的。也就是说,实现的是实调用而非虚调用。

看一个例子:

#include <iostream>
using namespace std;

class A {
public:
    virtual void show() {
        cout << "in A" << endl;
    }
    virtual ~A() { show(); }
};

class B :public A {
public:
    void show() {
        cout << "in B" << endl;
    }
};

int main() {
    A* a = new A();
    B* b = new B();

    delete a; // 输出:in A
    delete b; // 输出:in A
    system("pause");
}

在类 B 的对象 b 退出作用域时,会先调用类 B 的析构函数,然后调用类 A 的析构函数,在析构函数 ~A() 中,调用了虚函数 show()。从输出结果来看,类A的析构函数对 show() 调用并没有发生虚调用。

从概念上说,析构函数是用来销毁一个对象的,在销毁一个对象时,先调用该对象所属类的析构函数,然后再调用其基类的析构函数,所以,在调用基类的析构函数时,派生类对象的 “善后” 工作已经完成了,这个时候再调用在派生类中定义的函数已经没有意义了。


7 C++ 中构造函数和析构函数可以抛出异常吗?

C++ 并不禁止构造函数和析构函数抛出异常,所以都可以,但是都不建议。

  • 构造函数抛出异常时,析构函数将不会被执行,需要手动的去释放内存;

  • 析构函数抛出异常时,会导致程序过早结束或出现不明确的行为,甚至崩溃。


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

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

深拷贝的程序示例如下:

#include <iostream>
using namespace std;

class Class
{
public:
	Class(int b, char* cstr)
    {
        a = b;
        str = new char[b];
        strcpy(str, cstr);        
    }
    
	Class(const Class &C)
    {
        a = C.a;
        str = new char[a]; // 给str重新分配内存空间,所以为深拷贝 
        if (str != 0)
            strcpy(str, C.str);        
    }
    
	void Show()
    {
        cout << str << endl;
    }  
    
	~Class()
    {
		delete str;        
    }

private:
	int a;
	char *str;
};

int main()
{
	Class A(10, "hello"); // 输出:hello
	Class B = A;
	B.Show();

	return 0;
}

例如,在某些状况下,类内成员变量需要动态开辟堆内存,如果进行复制,也就是把对象里的值完全复制给另一个对象,如 A=B 时。如果没有自定义复制构造函数时, 系统将会提供给一个默认的复制构造函数来完成这个过程,就会造成 “浅拷贝”。

如果类 B 中有一个成员变量指针已经申请了内存,那么类 A 中的那个成员变量也指向同一块内存。这就出现了问题:例如当 B 通过析构函数把内存释放了,这时A 内的指针就变成野指针了,导致运行错误。所以要自定义赋值构造函数,重新分配内存资源,实现 “深拷贝”。


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

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

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

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

(3)基类的成员变量;

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


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

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

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

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

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


(1)为什么 C++ 不支持普通函数为虚函数?
普通函数(非成员函数)只能被重载,不能被重写,声明为虚函数也没有什么意义。


(2)为什么 C++ 不支持构造函数为虚函数?
虚函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用虚函数来完成你想完成的动作。(这不就是典型的悖论)


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


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


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


12 必须在构造函数初始化列表里进行初始化的数据成员有哪些?

必须在构造函数初始化列表里进行初始化的数据成员有常量成员、引用类型、对象成员。


13 memset 能否初始化类对象?

正常情况下,是可以使用 metset 初始化类对象的,但是如果该类含有虚函数,那么这个对象本身指向虚函数表的指针也会被初始化为 null,从而找不到该类的虚函数表,当我们想要利用该类发生多态时,程序崩溃,看例子。

class Animal {
public:
	virtual void walking() {
		cout << "animal walking" << endl;
	}
	void speak() {
		cout << "animal speaking" << endl;
	}
};

class People :public Animal {
public:
	virtual void walking() {
		cout << "people walking" << endl;
	}
	void speak() {
		cout << "people speaking" << endl;
	}
};

void demo() {
	People p;
	memset(&p, 0, sizeof(People));
	Animal *a = &p;
	a->speak(); // speak()不是虚函数 不影响
	a->walking(); // 程序直接崩溃
}

int main(){
	demo();
	return 0;
}

二、C 与 C++ 区别

1 C 和 C++ 有什么不同?

(1)机制不同:C是面向过程的;C++是面向对象的,提供了类。

(2)适用领域不同:C适合要求代码体积小的,效率高的场合,如嵌入式;C++适合更上层的,复杂的。

(3)侧重点不同:C语言是结构化编程语言,C++是面向对象编程语言。 C++侧重于对象而不是过程,侧重于类的设计而不是逻辑的设计。


2 C 和 C++ 中struct的区别是什么?

C 语言中的 struct 与 C++ 中的 struct 的区别表现在以下 3 个方面:

(1)C 语言的 struct 不能有函数成员,而 C++ 的 struct 可以有。

(2)C 语言的 struct 中数据成员没有 private、public 和 protected 访问权限的设定,而 C++ 的 struct 的成员有访问权限设定。

(3)C 语言的 struct 是没有继承关系的,而 C++ 的 struct 却有丰富的继承关系。

为了和 C 语言兼容,C++ 中就引入了 struct 关键字。C++ 语言中的 struct 是抽象数据类型 (ADT),它支持成员函数的定义,同时它增加了访问权限,它的成员函数默认访问权限为 public。


3 C++中的 struct与class的区别是什么?

具体而言,在 C++ 中,class 和 struct 做类型定义时只有两点区别:

(1)默认继承权限不同。class 继承默认是 private 继承,而 struct 继承默认是 public 继承;

(2)class 还用于定义模板参数,就像 typename,但关键字 struct 不用于定义模板参数。


4 简单描述一下你认为的C语言的优点和缺点?

优点:

(1)编写的程序可读性强,编译效率高;

(2)具有简洁紧凑、使用灵活的语法机制;

(3)允许直接访问物理地址,对硬件进行操作;

(4)数据结构丰富,满足多种数据开发要求;

(5)具有出色的可移植性,能在多种不同体系结构的软/硬件平台上运行。


缺点:

(1)C 语言的缺点主要表现在数据的封装性上,这一点使得 C 在数据的安全性上有很大缺陷,这也是 C 和 C++ 的一大区别。

(2)C 语言的语法限制不太严格,对变量的类型约束不严格,影响程序的安全性,比如对数组下标越界不作检查等。

(3)C 语言的简洁性与其丰富的运算符相结合,使其可能会编写出极难理解的代码。

(4)C 语言表达方面的自由会增加风险。尤其是C语言对指针的使用。


5 C++是不是类型安全的?

答案:不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。


三、一些注意事项

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

posted on 2023-04-22 21:47  长河饮马、  阅读(163)  评论(0编辑  收藏  举报

导航