快速查阅
C 语言和 C++ 语言的区别
C++诞生于C,是C的超集,拥有很多额外的特性。
- 语言范式 C是一种过程式编程语言,主要强调函数和过程的调用。它是一种底层语言,通常用于系统编程和嵌入式系统开发。C++是一种多范式编程语言,支持过程式编程、面向对象编程(OOP)和泛型编程。C++ 主要用于应用程序开发,特别是那些需要高性能的应用。类和对象是 C++ 的核心概念。
- 标准库 C提供了一个小而高效的标准库,主要包括标准输入输出(stdio.h)、字符串处理(string.h)、数学函数(math.h)等。C++提供了一个功能丰富的标准库,包括标准模板库(STL),其中包含了常用的数据结构(如向量、链表、集合和映射)和算法(如排序和搜索)。
- 内存管理 C内存管理是手动的,使用 malloc 和 free 进行动态内存分配和释放。C++除了手动内存管理(使用 new 和 delete),还支持智能指针(如 std::unique_ptr 和 std::shared_ptr),自动管理内存,减少内存泄漏的风险。
- 函数和操作符重载C不支持函数重载和操作符重载。C++支持函数重载(同名函数可以有不同的参数列表)和操作符重载(可以重定义操作符的行为)
- 命名空间 C没有命名空间,所有的标识符在全局作用域中。C++支持命名空间(namespace),可以更好地组织代码,避免命名冲突。
- 模板 C不支持模板。C++支持模板,允许编写泛型代码,可以用于创建通用的数据结构和算法。
- 异常处理 C没有内置的异常处理机制,通常使用错误码来表示错误。C++提供了异常处理机制(try、catch 和 throw),可以捕获和处理运行时错误。
- 兼容性 C++ 是对 C 的超集,绝大部分 C 代码可以在 C++ 编译器下编译运行,且 C++ 有更多的语法和特性。
说说指针和引用的区别
首先,指针和引用都是用于处理内存地址的概念。指针是一个变量,其值是另一个变量的地址,它允许直接访问内存中的数据。引用则是一个变量的别名,它代表了另一个变量的内存位置。
- 访问值 指针可以通过解引用运算符 * 访问指针所指向的值,引用则直接访问所引用变量的值,不需要解引用操作符。
- 声明赋值 type *ptr;,其中 type 是指向的数据类型,表示指针,可以通过取地址运算符 & 获取变量的地址,并将其赋值给指针,可以用 nullptr 或 NULL 表示空指针。type& ref = var;,其中 type 是引用的类型,& 表示引用,一旦初始化,引用不允许重新绑定,它总是指向同一个对象,且不存在空引用,引用必须在声明时初始化,。
- 使用场景 指针通常用于动态内存分配、传递数组、在函数中返回多个值等情况。引用通常用于函数参数传递、避免对象拷贝、重载操作符、创建别名等情况。
总而言之,引用更加安全,语法更加简洁清晰。
说说const和define的区别
- 本质区别 #define 是一种预处理器指令,在编译器处理源代码之前,由预处理器进行简单的文本替换。const 是一个关键字,编译器在编译时处理它。
- 类型安全 const 是一种类型安全的方式来定义常量,#define 是一种预处理器指令,它在编译之前进行文本替换,没有类型信息。
- 作用域 const 常量有作用域,可以是全局的或局部的,取决于它的定义位置,也就是遵循 C++ 的作用域规则。#define 定义的宏在整个文件范围内有效,除非用 #undef 取消定义。
- 调试和编译 由于 const 是类型安全的,在编译期就可以检查类型错误。#define 没有类型检查,容易导致难以发现的错误。
- 内存占用 const 常量在大多数情况下分配内存,尤其是在其地址被取用时(&x)(编译时常量折叠,内联优化,常量折叠和优化可不分配,依赖编译器优化技术)#define 不占用内存,因为它只是简单的文本替换,因此用 define 定义的常量是不可以用指针变量去指向的,用 const 定义的常量是可以用指针去指向该常量的地址的。
- 使用范围 const 可以用于任何类型,包括结构体、类、基本数据类型等,可以用在类的成员变量中,使成员变量成为常量。#define 通常用于简单的常量定义和代码片段的替换,不适合复杂类型或类成员变量的定义。
说说new和malloc的区别
在C++中,new运算符和malloc函数都是用于动态内存分配的。简单的说new = malloc + 构造函数,delete = free + 析构函数。C没有new/delete,C++为了支持混编,保留了malloc/free函数,malloc只分配内存,不会初始化对象或基本数据类型的值,并且需要用sizeof手动计算大小,由于返回的是通用指针类型void*所以还要进行强制类型转换。
C++ 和 C中struct的区别
C中的struct是一种聚合数据类型,允许将不同类型的数据组合在一起。它没有面向对象编程的特性,主要用于数据的打包和管理。
- 默认访问控制 所有成员都是公有的(public),不支持访问控制关键字public、private 和 protected。
- 不能包含成员函数 只能包含数据成员(变量)也不能定义构造函数和析构函数
- 不支持继承 struct 无法继承其他 struct
C++中的struct 不仅可以包含数据成员,还可以包含成员函数。此外,C++ 的 struct 是一个完整的类,支持面向对象编程的所有特性。
- 默认访问控制 所有成员都是公有的(public),与 C 中一样。唯一的区别是 C++ 中的 class 默认是私有的(private)。并且支持访问控制关键字,可以使用 public、private 和 protected。
- 可以包含成员函数 可以包含构造函数、析构函数、成员函数等。
- 支持继承 struct 可以继承其他 struct 或 class
C++中的struct更加强大,提供了许多面向对象编程的特性,而 C 的 struct 则更加简单,主要用于数据的组织和管理。
谈谈struct和class的区别
首先,两者都可以包含数据成员和成员函数,都支持继承,包括单继承和多继承,都可以使用 public、protected 和 private 关键字来控制成员的访问权限,都可以定义构造函数、析构函数和拷贝构造函数,都可以使用虚函数来实现多态。
不同点在于:
- 默认访问控制 struct默认的访问控制是公有的(public),而class默认的访问控制是私有的(private)
通常使用 struct 时,开发者通常期望简单的数据存储和传输,而不会涉及复杂的行为和封装。使用 class 时,开发者通常期望定义一个具有复杂行为和封装的对象,可能包含多种方法和私有数据。两者的使用场景和开发意图并不相同
谈谈浅拷贝和深拷贝的区别
浅拷贝和深拷贝是两种不同的对象拷贝方式。它们的主要区别在于是否拷贝对象所指向的资源(如动态分配的内存)。
- 浅拷贝是指直接复制对象的所有成员变量的值,包括指针成员的地址,通常由编译器提供的默认拷贝构造函数和赋值运算符完成。这意味着浅拷贝后的两个对象共享相同的资源。浅拷贝的问题在于,当一个对象被销毁时,它会释放共享的资源,从而导致另一个对象访问无效的内存。
#include <iostream>
#include <cstring>
class Shallow {
public:
char* data;
Shallow(const char* input) {
data = new char[strlen(input) + 1];
strcpy(data, input);
}
// 编译器提供的默认拷贝构造函数
Shallow(const Shallow& other) = default;
~Shallow() {
delete[] data;
}
void print() const {
std::cout << data << std::endl;
}
};
int main() {
Shallow obj1("Hello");
Shallow obj2 = obj1; // 使用默认的浅拷贝
obj1.print();
obj2.print();
return 0;
}
- 深拷贝是指不仅复制对象的所有成员变量的值,还要复制对象所指向的资源,需要显式定义拷贝构造函数和赋值运算符,以确保复制对象所指向的资源。这意味着深拷贝后的两个对象拥有独立的资源,不会互相影响。
#include <iostream>
#include <cstring>
class Deep {
public:
char* data;
Deep(const char* input) {
data = new char[strlen(input) + 1];
strcpy(data, input);
}
// 深拷贝构造函数
Deep(const Deep& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// 深拷贝赋值运算符
Deep& operator=(const Deep& other) {
if (this == &other) {
return *this;
}
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
return *this;
}
~Deep() {
delete[] data;
}
void print() const {
std::cout << data << std::endl;
}
};
int main() {
Deep obj1("Hello");
Deep obj2 = obj1; // 使用深拷贝构造函数
obj1.print();
obj2.print();
return 0;
}
基于此,浅拷贝和深拷贝各有其适用场景。在需要复制包含指针成员或动态内存分配的对象时,应使用深拷贝以避免内存管理问题。而对于简单对象或不涉及动态内存的对象,浅拷贝通常是足够的。使用深拷贝时要留意内存释放,避免内存泄漏问题以及自赋值问题还有异常安全性,用现代C++技术中的智能指针能自动管理内存,确保内存安全。
谈谈 C++ 的内存管理
首先,C++的超绝性能来源于于对内存的支配权和更大程度的自由,所以内存管理显然是C++最要害的问题,要做内存管理首先得了解C++的内存结构,它通常有以下几个主要区域:
- 代码段 存储程序的机器代码(也就是编译后的指令),确保程序能够正确执行
- 数据段 可细分为存储已初始化的静态和全局变量的已初始化数据段和存储未初始化的静态和全局变量,程序启动时会将其初始化为零的未初始化数据段
- 栈 遵循后进先出(LIFO)原则,速度较快,用于管理函数调用过程中分配的局部变量、函数参数和返回地址。栈内存的分配和释放由编译器自动管理。通常向低地址方向生长,由处理器架构的设计决定的,栈的内存分配是通过将栈指针减小来实现。
- 堆 用于动态分配内存,存储需要动态分配的对象和数据,需要显式分配和释放内存(如使用 new 和 delete)管理复杂,可能导致内存碎片和泄漏。向高地址方向生长,由操作系统的虚拟内存管理器决定,因为堆内存是在操作系统的虚拟内存空间中动态分配的,而虚拟内存的分配通常是从高地址向低地址进行的。
- 常量区 存储常量和字面值,通常是只读的,和代码段类似,常量区中的数据不会被修改。
另外,由于堆和栈生长方向的不同,堆和栈的内存空间通常会在某个点相遇,这种情况被称为内存碰撞。操作系统会在堆和栈之间留下一段保护区域,以防止它们相互覆盖。
C++常见内存泄漏类型,原因,解决
C/C++开发人员,内存泄漏是最容易遇到的问题之一,这是由C/C++语言的特性引起的,与其他语言不同,需要开发者去申请和释放内存,即需要开发者去管理内存,如果内存使用不当,就容易造成段错误或者内存泄漏。
段错误是一种访问内存的错误。当程序试图访问未分配的内存或以不正确的方式访问内存时,会发生的错误,通常立即导致程序崩溃,并产生核心转储(是程序在异常终止时,操作系统生成的一种文件,包含了程序在崩溃时的内存内容,包括堆栈、寄存器、程序计数器和其他与进程相关的信息。核心转储文件对于调试程序非常有用,可以帮助开发者找到程序崩溃的原因)。常见的段错误有:
- 空指针解引用 尝试访问通过空指针指向的内存地址导致的。
- 野指针解引用 指针没有被初始化就被使用。
- 越界访问数组 访问数组范围之外的元素。
- 释放后访问 释放内存后继续访问这段内存。
- 栈溢出 递归太深或分配过大的局部变量,导致栈内存耗尽。
内存泄漏是指程序中动态分配的内存未能被正确释放,导致这部分内存不能被重用。在长时间运行的程序中,内存泄漏会逐渐耗尽系统内存,导致性能下降,甚至程序崩溃,影响程序的长期运行和性能。常见的内存泄漏有:
- 未释放动态分配的内存,使用 malloc、calloc 或 realloc 分配的内存没有使用 free 释放。使用 new 分配的内存没有使用 delete 或 delete[] 释放。
- 循环引用 两个或多个对象相互引用对方,导致垃圾回收器无法回收这些对象。
- 异常处理不当 在异常抛出后,动态分配的内存没有被释放
- 内存管理错误 重复释放同一块内存,释放未分配的内存。
预防和处理段错误的措施:
- 养成良好的编程习惯,如有malloc/new,就得有free/delete,谁申请谁释放等,遵守编程规范,如RAII(资源获取即初始化)原则等。
- 初始化指针和边界检查,确保使用指针和访问元素前检查边界保证不越界。
- 使用智能指针来自动管理内存。
- 使用工具和库,AddressSanitizer ,Valgrind,日志等工具来检测内存问题,使用标准库中的容器(如 std::vector、std::string)来避免手动管理内存,使用静态分析工具(如 Clang-Tidy 和 Cppcheck)进行代码审查。
谈谈 C++ 面向对象
首先,先说说为什么要面向对象,其意为何,OOP是为了解决一些面向过程编程(Procedural Programming)在软件开发中的局限性。具体来说,面向对象编程主要解决以下几个问题:
- 代码可重用性,面向过程编程中,代码重用性较低,通常需要重复编写相似的代码。面向对象编程通过类和继承机制,使得代码可以在不同的项目中重用,从而减少了代码的重复编写。
- 代码可维护性,随着软件项目规模的增大,代码变得难以维护和管理。面向对象编程通过封装、继承和多态等特性,使得代码结构更加清晰,易于理解和维护。
- 代码可扩展性,在面向过程编程中,添加新的功能通常需要修改现有的代码,容易引入错误。面向对象编程通过类的继承和多态机制,使得新的功能可以通过扩展已有的类来实现,减少了对现有代码的影响。
- 数据和功能的结合,面向过程编程中,数据和操作分离,不利于数据的保护和管理。面向对象编程通过将数据和操作封装在对象中,提供了更好的数据保护和管理机制。
- 模拟现实世界:面向对象编程通过类和对象的概念,更加贴近现实世界中的事物和行为,便于设计和理解复杂的软件系统。
面向对象编程 (OOP) 具有四个核心概念,它们是:封装、继承、多态和抽象。这些概念是构建面向对象程序的基础。
- 封装是指将对象的状态(属性)和行为(方法)结合在一起,并对外隐藏内部实现细节。通过封装,可以保护对象的状态不被外部直接修改,只能通过对象提供的方法来操作。
class Person {
// 封装
private:
std::string name;
int age;
public:
void setName(const std::string& n) {
name = n;
}
std::string getName() const {
return name;
}
void setAge(int a) {
if (a > 0) {
age = a;
}
}
// get方法
int getAge() const {
return age;
}
};
- 继承允许一个类(子类)从另一个类(父类)获取属性和方法。通过继承,可以重用已有代码,并在此基础上进行扩展。
class Animal {
public:
void eat() {
std::cout << "Eating..." << std::endl;
}
};
// 继承
class Dog : public Animal {
public:
void bark() {
std::cout << "Barking..." << std::endl;
}
};
- 多态允许同一个接口调用不同的实现。多态性通常通过基类指针或引用来实现,能够在运行时动态决定调用哪个子类的实现。另外,虚函数是实现多态性的重要机制。在基类中使用关键字 virtual 声明的函数称为虚函数。通过虚函数,派生类可以重写基类的方法,而在使用基类指针或引用时,调用的是对象的实际类型的重写方法,而不是基类的方法。
class Animal {
public:
virtual void makeSound() const {
std::cout << "Some generic animal sound" << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() const override {
std::cout << "Woof! Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() const override {
std::cout << "Meow! Meow!" << std::endl;
}
};
void makeAnimalSound(const Animal& animal) {
animal.makeSound();
}
// 基类引用
int main() {
Dog dog;
Cat cat;
makeAnimalSound(dog); // 输出: Woof! Woof!
makeAnimalSound(cat); // 输出: Meow! Meow!
return 0;
}
// 基类指针
int main() {
Animal* animalPtr;
Dog dog;
Cat cat;
animalPtr = &dog;
animalPtr->makeSound(); // 输出: Woof! Woof!
animalPtr = &cat;
animalPtr->makeSound(); // 输出: Meow! Meow!
return 0;
}
- 抽象是指隐藏对象的复杂实现细节,只向外部提供必要的接口。抽象通过抽象类或接口来实现。
class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing Rectangle" << std::endl;
}
};
int main() {
Circle circle;
Rectangle rectangle;
circle.draw(); // 输出: Drawing Circle
rectangle.draw(); // 输出: Drawing Rectangle
return 0;
}
很枯燥,这就是八股文 :(