1.C 和 C++ 最广泛的区别总结
C 和 C++ 的主要区别在于编程范式和语言特性。C 作为结构化编程语言,主要强调过程和算法;而 C++ 则引入了面向对象编程,增加了封装、继承和多态的特性,使代码更具灵活性和复用性。C++ 支持函数重载、虚函数、模板编程和异常处理,并提供了丰富的标准库和高级的输入输出流管理,使编程更高效便捷。C++ 还引入了命名空间、智能指针、构造函数和析构函数,支持多重继承和复杂的类型系统,使内存管理更安全,代码更易于维护和扩展。
1. 编程范式
- C:结构化编程,强调过程和算法。
- C++:面向对象编程,强调对象模型和类,同时支持结构化编程。
2. 语言特性
- C:注重过程和算法。
- C++:在此基础上增加了封装、继承和多态三大面向对象特性。
3. 函数和方法
- C:不支持函数重载和虚函数。
- C++:支持函数重载和虚函数,通过虚函数表实现多态。
4. 动态内存管理
- C:使用
malloc
和free
进行内存管理。 - C++:增加了
new
和delete
,进行更安全和方便的内存管理。
5. 模板和泛式编程
- C:不支持模板和泛式编程。
- C++:支持模板,使得编写泛型代码更加容易。
6. 标准库
- C:标准库较为基础。
- C++:提供了丰富的 STL,包括容器、算法、迭代器等。
7. 错误处理
- C:使用返回值和
errno
。 - C++:引入了异常处理机制,如
try
,catch
,throw
。
8. 输入输出
- C:使用
printf
和scanf
进行输入输出。 - C++:使用
cin
和cout
进行输入输出,本质上是输入输出流,同时支持更高级的输入输出流,如fstream
用于文件操作。
9. 命名空间
- C:不支持命名空间。
- C++:引入命名空间,用于组织和管理代码,避免命名冲突。
10. 智能指针
- C:不支持智能指针。
- C++:C++11 引入智能指针,如
std::unique_ptr
和std::shared_ptr
,更好地管理内存,防止内存泄漏和悬空指针。
11. 构造函数和析构函数
- C:不支持构造函数和析构函数。
- C++:引入构造函数和析构函数用于对象的初始化和清理。
2.C++ 和 Java 区别总结
1. 编程范式
- C++:支持面向对象编程和结构化编程。
- Java:完全面向对象编程。
2. 指针
- C++:支持指针,允许直接操作内存。
- Java:不支持指针,内存管理由虚拟机自动处理。
3. 多重继承
- C++:支持多重继承。
- Java:不支持多重继承,但支持一个类实现多个接口。
4. 自动内存管理
- C++:手动管理内存,使用
new
和delete
。 - Java:自动内存管理,使用垃圾回收机制。
5. 操作符重载
- C++:支持操作符重载。
- Java:不支持操作符重载。
6. 预处理功能
- C++:支持预处理器,宏定义、文件包含等。
- Java:不支持预处理器,但使用
import
进行包管理。
7. 类型转换
- C++:支持隐式和显式类型转换。
- Java:需要显式的强制类型转换。
8. 字符串
- C++:使用 Null 终止符表示字符串结束。
- Java:使用
String
和StringBuffer
类表示字符串。
9. Goto 语句
- C++:支持
goto
语句,但不推荐使用。 - Java:不支持
goto
语句,尽管是保留关键字。
10. 异常机制
- C++:支持异常处理机制(
try
,catch
,throw
)。 - Java:支持异常处理机制,增强系统容错能力。
11. 输入输出
- C++:使用
printf
和scanf
,以及面向对象的cin
和cout
流操作。 - Java:使用
System.out.print
和System.in.read
,主要通过流进行操作。
12. 智能指针
- C++:C++11 引入智能指针(
std::unique_ptr
,std::shared_ptr
)管理内存。 - Java:自动内存管理,不需要智能指针。
13. 构造函数和析构函数
- C++:支持构造函数和析构函数用于对象的初始化和清理。
- Java:支持构造函数,没有显式的析构函数,由垃圾回收机制管理对象的销毁。
3.面向对象的三大特性
C++ 语言的面向对象编程(OOP)有三大特性:封装、继承、多态。以下是对这三大特性的详细说明。
1. 封装
概念: 封装是将数据和操作这些数据的代码捆绑在一起,并对外部世界隐藏实现细节的一种方法。通过封装,类可以控制哪些数据和方法对外部可见,哪些是私有的。封装确保了对象的完整性和安全性,使得对象的使用者不需要了解内部实现细节。
详细说明:
- 封装通过访问控制(如
private
,protected
,public
)来实现数据的隐藏和保护。 - 公有接口(
public
方法)提供了与对象交互的方式,而私有成员(private
数据和方法)则保护了对象的内部状态。 - 通过封装,可以防止对象的内部状态被意外或错误地修改,增强代码的安全性和可维护性。
2. 继承
概念: 继承是从已有类创建新类的机制。通过继承,新类(派生类)可以继承已有类(基类)的属性和方法,并可以扩展或重写这些属性和方法。继承的方式(如公有、保护、私有继承)决定了基类成员在子类中的可见性和访问权限。
详细说明:
- 继承实现了代码重用和层次分类,通过继承,派生类可以复用基类的代码。
- 派生类可以通过重写基类的虚函数来提供自己的实现,增强或改变基类的方法。
- 继承的方式(
public
、protected
、private
继承)控制了基类成员在派生类中的可见性:- 公有继承(
public
):基类的公有成员在派生类中仍然是公有的,保护成员在派生类中仍然是保护的。 - 保护继承(
protected
):基类的公有成员和保护成员在派生类中都变成保护的。 - 私有继承(
private
):基类的所有成员在派生类中都变成私有的。
- 公有继承(
3. 多态
概念: 多态性是指同一个操作在不同对象上的不同表现形式。多态分为编译时多态(如函数重载和运算符重载)和运行时多态(如通过基类指针调用派生类重写的虚函数)。
详细说明:
- 编译时多态:通过函数重载和运算符重载实现,函数根据参数类型和数量的不同实现不同的功能。
- 运行时多态:通过虚函数实现,基类指针或引用在运行时可以调用派生类重写的虚函数。
- 虚函数通过虚函数表(vtable)和虚函数表指针(vptr)实现,编译器在编译时创建虚函数表,在运行时通过虚函数表指针调用实际的函数实现。
- 当基类指针或引用指向派生类对象时,调用虚函数会动态绑定到派生类的实现上,实现在运行时确定实际调用的函数。
4.类和数据抽象总结
1. 类的关系
组合
- 定义:一个类的数据成员是另一个类的对象。组合类是当前类,它包含其他类的对象作为其成员。
- 创建过程:先调用内嵌对象(成员对象)的构造函数,再调用组合类(当前类)的构造函数。
- 析构过程:先执行组合类(当前类)的析构函数,再按内嵌对象(成员对象)成员的定义顺序逆序调用内嵌对象的析构函数。这意味着析构函数也是从内向外调用,即从成员对象开始,逐步到组合类。
使用
- 定义:一个类通过参数、函数调用等方式使用另一个类的对象。
- 创建过程:使用对象的创建通常在需要时进行,不直接影响当前类的创建过程。
- 析构过程:使用对象在超出作用域或不再需要时自动销毁,不影响当前类的析构过程。使用对象的析构是根据其作用域决定的。
继承
- 定义:一个类通过继承另一个类的属性和方法,形成父类和子类的关系。
- 创建过程:先调用父类的构造函数,再调用子类的构造函数。
- 析构过程:先执行子类的析构函数,再按继承链顺序逆序调用父类的析构函数。这意味着析构函数是从最派生的类开始,逐层向基类调用。
2. 三种继承方式
- 公有继承:父类的公有成员在子类中保持公有,保护成员在子类中保持保护。
- 保护继承:父类的公有成员和保护成员在子类中都变为保护。
- 私有继承:父类的公有成员和保护成员在子类中都变为私有。
3.C++ 中重载、重写和重定义的区别
1. 重载(Overload)
- 定义:在同一作用域中,具有相同名称但参数列表不同的一组函数。
- 特点:
- 参数列表必须不同,可以是参数类型、参数个数或参数顺序不同。
- 重载函数的返回类型可以相同也可以不同,但返回类型不能用于区分重载函数。
- 函数名相同但参数列表不同的函数。
- 用途:提供多个同名函数,以处理不同类型或数量的参数。
- 示例:
#include <iostream> class Base { public: // 基类中的虚函数 virtual void func(int a) { std::cout << "Base func(int): " << a << std::endl; } }; class Derived : public Base { public: // 派生类重写基类的虚函数 void func(int a) override { std::cout << "Derived func(int): " << a << std::endl; } // 派生类重载基类的虚函数 void func(double a) { std::cout << "Derived func(double): " << a << std::endl; } // 派生类重载基类的虚函数 void func(int a, double b) { std::cout << "Derived func(int, double): " << a << ", " << b << std::endl; } }; int main() { Derived d; // 调用 Derived 中重写的函数 d.func(10); // 输出:Derived func(int): 10 // 调用 Derived 中重载的函数 d.func(3.14); // 输出:Derived func(double): 3.14 d.func(10, 3.14); // 输出:Derived func(int, double): 10, 3.14 // 通过基类指针调用函数 Base* b = &d; b->func(10); // 输出:Derived func(int): 10 (多态性) return 0; }
(1)为什么前置自加需要返回引用,而后置自加不需要?
前置自加:
- 直接修改对象并返回修改后的对象本身。
- 返回引用避免了不必要的对象拷贝,提升性能。
- 允许链式调用(如
++(++x)
)。 Counter& operator++() { ++value; // 先增加对象的值 return *this; // 返回对象本身的引用 }
后置自加:
- 返回操作前的状态,因此必须创建一个临时对象。
- 临时对象是一个值,返回后不会影响原始对象。
- 返回值不能直接影响链式调用,需要额外的临时存储
Counter operator++(int) { Counter temp = *this; // 保存当前对象的副本 ++value; // 增加对象的值 return temp; // 返回保存的副本 }
(2)提取运算符与插入运算符重载
插入运算符重载:
- 定义为友元函数,接受
std::ostream
引用和Complex
对象引用作为参数。 - 使用
ostream
对象将Complex
对象的real
和imag
成员输出到流中。
- 定义为友元函数,接受
提取运算符重载:
- 定义为友元函数,接受
std::istream
引用和Complex
对象引用作为参数。 - 使用
istream
对象从流中提取Complex
对象的real
和imag
成员。
- 定义为友元函数,接受
/* os 是提取运算符(<<)的调用对象,表示从哪个输入流读取数据。
c 是提取运算符的目标对象,表示读取的数据将存储到该对象中。 */
// 重载插入运算符 <<
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
/* is 是提取运算符(>>)的调用对象,表示从哪个输入流读取数据。
c 是提取运算符的目标对象,表示读取的数据将存储到该对象中。 */
// 重载提取运算符 >>
friend std::istream& operator>>(std::istream& is, Complex& c) {
is >> c.real >> c.imag;
return is;
}
2. 重写(Override)
- 定义:在派生类中重新定义基类中的虚函数。
- 特点:
- 被重写的函数必须是虚函数(
virtual
),并且派生类中的函数必须具有相同的函数签名(参数列表和返回类型)。 - 重写的函数名和参数列表必须与基类中的虚函数完全相同。
- 基类中的虚函数在派生类中被重新定义以提供不同的实现。
- 被重写的函数必须是虚函数(
- 用途:提供多态行为,通过基类指针或引用调用派生类的方法。
- 示例:
class Base { public: virtual void func() {} }; class Derived : public Base { public: void func() override {} // 重写基类的虚函数 };
3. 重定义(隐藏)(Redefine/Hide)
- 定义:在派生类中定义与基类中名称相同但非虚的函数。
- 特点:
- 重定义的函数在基类中不是虚函数。
- 派生类中的函数隐藏基类中的同名函数,无论参数列表是否相同。
- 如果基类中的函数是非虚函数,派生类中定义的同名函数会隐藏基类中的函数。
- 参数列表和返回类型可以不同。
- 用途:隐藏基类中的同名函数,提供新的实现。
- 示例:
#include <iostream> class Base { public: void func() { std::cout << "Base func()" << std::endl; } void func(int x) { std::cout << "Base func(int)" << std::endl; } }; class Derived : public Base { public: void func() { std::cout << "Derived func()" << std::endl; } void func(double y) { std::cout << "Derived func(double)" << std::endl; } }; int main() { Derived d; d.func(); // 调用 Derived::func() d.func(3.14); // 调用 Derived::func(double) d.Base::func(10); // 调用 Base::func(int) return 0; }
4.析构函数
(1)析构函数的作用
析构函数在对象的生命周期结束时被调用,主要用于清理和释放对象占用的资源。其主要作用包括:
- 释放动态分配的内存:如果对象在构造函数或成员函数中动态分配了内存,析构函数应该负责释放这些内存,以防止内存泄漏。
- 关闭文件或网络连接:如果对象打开了文件或建立了网络连接,析构函数应该负责关闭这些资源。
- 释放其他系统资源:如锁、线程或其他系统资源。
(2)析构函数的特点
- 名称:析构函数与类名相同,但前面加上波浪号
~
。 - 没有参数和返回值:析构函数不能有参数,也没有返回值。
- 不能重载:一个类只能有一个析构函数。
- 自动调用:当对象超出其作用域或显式删除对象时,编译器自动调用析构函数。
5.构造函数和析构函数的执行顺序
(1)构造函数的执行顺序
- 基类构造函数:首先调用基类的构造函数。如果有多个基类,则按类派生表中出现的顺序依次调用。
- 成员类对象构造函数:然后调用成员类对象的构造函数。如果有多个成员类对象,则按它们在类中声明的顺序依次调用。
- 派生类构造函数:最后调用派生类的构造函数。
(2)析构函数的执行顺序
- 派生类析构函数:首先调用派生类的析构函数。
- 成员类对象析构函数:然后调用成员类对象的析构函数。如果有多个成员类对象,则按它们在类中声明的顺序相反的顺序依次调用。
- 基类析构函数:最后调用基类的析构函数。
6.纯虚函数
纯虚函数用于面向对象编程中的接口继承和实现继承,允许派生类根据需要继承接口和/或实现。
(1)定义
- 纯虚函数:在基类中声明但不实现,语法为
virtual void function() = 0;
。
作用:纯虚函数的存在表明基类提供了一个接口,要求所有派生类必须实现这个函数。基类只是定义了函数的接口,而没有提供具体的实现。这种设计用于确保派生类必须提供自己的实现,从而实现多态性。
抽象类
定义:抽象类是包含一个或多个纯虚函数的类。由于纯虚函数没有实现,因此抽象类不能实例化对象。
作用:抽象类提供了一个框架,定义了派生类必须实现的接口。它们用于定义派生类的公共接口,并且无法创建抽象类的对象,因为其接口不完整。
(2)应用
- 接口继承:纯虚函数仅提供接口,派生类必须实现这些函数才能实例化。
#include <iostream> // 基类 class Animal { public: virtual void makeSound() = 0; // 纯虚函数 }; // 派生类 Dog class Dog : public Animal { public: void makeSound() override { std::cout << "Woof!" << std::endl; } }; // 派生类 Cat class Cat : public Animal { public: void makeSound() override { std::cout << "Meow!" << std::endl; } }; int main() { Dog dog; Cat cat; dog.makeSound(); // 输出 "Woof!" cat.makeSound(); // 输出 "Meow!" return 0; }
- 实现继承:基类可以提供函数的默认实现,派生类可选择重写或使用基类实现。
#include <iostream> // 基类 class Animal { public: virtual void makeSound() { std::cout << "Some generic animal sound!" << std::endl; } }; // 派生类 Dog class Dog : public Animal { public: void makeSound() override { std::cout << "Woof!" << std::endl; } }; // 派生类 Sheep 使用基类的实现 class Sheep : public Animal { // 不重写 makeSound }; int main() { Dog dog; Sheep sheep; dog.makeSound(); // 输出 "Woof!" sheep.makeSound(); // 输出 "Some generic animal sound!" return 0; }
- 禁止重写:使用
final
关键字可以防止派生类重写基类的实现。 #include <iostream> // 基类 class Animal { public: virtual void makeSound() final { std::cout << "Some generic animal sound!" << std::endl; } }; // 派生类 Dog 不能重写 makeSound class Dog : public Animal { public: // void makeSound() override { // 错误:makeSound 被声明为 final // std::cout << "Woof!" << std::endl; // } }; int main() { Dog dog; dog.makeSound(); // 输出 "Some generic animal sound!" return 0; }
(3)理解纯虚函数的默认实现
虽然纯虚函数通常在基类中声明但不实现,但在某些情况下,基类也可以为纯虚函数提供一个默认实现。派生类在需要时可以显式调用这个基类的默认实现。这种设计既允许派生类提供自己的实现,又可以在必要时调用基类提供的默认行为。
(4)实现细节
- 纯虚函数的声明和默认实现:在基类中声明纯虚函数,并在类外提供其默认实现。
- 派生类的实现:派生类可以选择重写纯虚函数,也可以调用基类的默认实现。
- 在基类中调用纯虚函数:基类可以在其成员函数中调用纯虚函数的默认实现。
1.基类提供纯虚函数及其默认实现
#include <iostream>
class Animal {
public:
virtual void makeSound() = 0; // 纯虚函数
void callBaseSound() {
Animal::makeSound(); // 调用基类的默认实现
}
};
// 基类中纯虚函数的默认实现
void Animal::makeSound() {
std::cout << "Some generic animal sound!" << std::endl;
}
2.派生类实现纯虚函数
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "Meow!" << std::endl;
}
};
class Sheep : public Animal {
// 使用基类的默认实现,不重写 makeSound
};
3.使用多态性调用纯虚函数
int main() {
Dog dog;
Cat cat;
Sheep sheep;
dog.makeSound(); // 输出 "Woof!"
cat.makeSound(); // 输出 "Meow!"
sheep.makeSound(); // 输出 "Some generic animal sound!"
// 基类中的成员函数调用纯虚函数的默认实现
sheep.callBaseSound(); // 输出 "Some generic animal sound!"
return 0;
}
7.静态绑定和动态绑定概述
静态绑定(早绑定)
- 定义:静态绑定在编译期间确定函数调用或属性访问,绑定的是对象的静态类型(声明的类型)。
- 特点:
- 编译期确定:函数调用在编译时决定。
- 非虚函数和缺省参数值:非虚函数和默认参数值在编译期绑定。
- 基类指针调用基类版本:即使指针指向派生类对象,但调用非虚函数时仍是基类版本。
#include <iostream>
class Base {
public:
void show() {
std::cout << "Base::show()" << std::endl;
}
};
class Derived : public Base {
public:
void show() {
std::cout << "Derived::show()" << std::endl;
}
};
int main() {
Base* b = new Derived();
b->show(); // 输出 "Base::show()",因为 show 是静态绑定
delete b;
return 0;
}
动态绑定(晚绑定)
- 定义:动态绑定在运行期间确定函数调用或属性访问(访问对象的成员变量),绑定的是对象的动态类型(实际类型)。
- 特点:
- 运行时确定:函数调用在运行时决定。
- 通过虚函数实现:使用
virtual
关键字声明函数实现动态绑定。 - 支持多态性:虚函数调用根据对象的动态类型,支持多态行为。
#include <iostream>
class Base {
public:
virtual void show() {
std::cout << "Base::show()" << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived::show()" << std::endl;
}
};
int main() {
Base* b = new Derived();
b->show(); // 输出 "Derived::show()",因为 show 是虚函数,采用动态绑定
delete b;
return 0;
}