C++面试

C和C++的区别

C 和 C++ 是两种编程语言,它们之间有一些显著的区别:

1. 编程范式

  • C: 是一种过程式编程语言,强调面向过程的编程,程序是通过函数调用和控制流程(如循环、条件语句等)来实现的。
  • C++: 是一种多范式编程语言,支持面向对象编程(OOP)、泛型编程和过程式编程。面向对象编程允许开发者使用类和对象来组织代码,提高了代码的可重用性和可维护性。

2. 类和对象

  • C: 不支持类和对象。所有的数据结构和操作都是通过结构体和函数实现的。
  • C++: 引入了类和对象的概念,支持封装、继承和多态性等面向对象的特性。类允许开发者定义数据和行为的封装体,使得代码更具模块化。

3. 函数重载和运算符重载

  • C: 不支持函数重载和运算符重载。每个函数在同一个作用域内必须具有唯一的名称。
  • C++: 允许函数重载,即在同一个作用域内可以有多个同名函数,但它们的参数列表必须不同。C++ 还支持运算符重载,使得可以对自定义类型进行运算符操作。

4. 标准库

  • C: 标准库较为简单,主要包括输入/输出(如 stdio.h)、字符串操作(如 string.h)、数学计算(如 math.h)等。
  • C++: 标准库比 C 丰富得多,除了 C 的标准库外,还包括标准模板库(STL),提供了丰富的数据结构(如 vector, map 等)和算法(如 sort, search 等),提高了编程效率。

5. 内存管理

  • C: 手动管理内存,使用 mallocfree 分配和释放动态内存。
  • C++: 除了 C 的内存管理方式外,C++ 还引入了 newdelete 操作符用于动态内存管理。C++ 中的构造函数和析构函数可以在对象创建和销毁时自动调用,方便资源的管理。

6. 命名空间

  • C: 不支持命名空间,所有的全局变量和函数都在同一个全局命名空间内。
  • C++: 引入了命名空间的概念,可以用来避免命名冲突,使得大型项目的代码组织更为清晰。

7. 模板

  • C: 不支持模板。函数和数据结构的泛型编程需要手动实现。
  • C++: 支持模板,允许编写泛型代码,使得函数和类可以用于任意类型,提高了代码的通用性。

8. 异常处理

  • C: 不支持异常处理,通常使用返回值或错误代码来进行错误处理。
  • C++: 支持异常处理,提供了 try, catch, throw 语句,用于在运行时处理异常情况,提高了代码的健壮性。

9. 编译速度

  • C: 由于语法相对简单,编译速度通常较快。
  • C++: 由于引入了更多复杂的特性(如面向对象、模板等),编译速度通常比 C 慢。

10. 应用场景

  • C: 通常用于系统编程,如操作系统、嵌入式系统等,因其高效、接近底层。
  • C++: 广泛用于开发大型软件系统、游戏引擎、图形处理等,得益于其面向对象和泛型编程的优势。

总结来说,C++ 在 C 的基础上引入了许多高级特性,使其更适合大型和复杂系统的开发,而 C 则以其简洁和高效,继续在底层系统开发中保持着重要地位。

指针和引用的区别

在 C++ 中,指针和引用是两种用于间接访问变量的机制,但它们有一些关键的区别:

1. 定义和语法

  • 指针:指针是一个变量,它存储另一个变量的内存地址。指针需要通过解引用操作符 * 来访问所指向的变量的值。
int a = 10;
int* p = &a; // p 是指向 a 的指针
  • 引用:引用必须在定义时初始化,并且一旦绑定到某个变量,就不能再更改引用的对象。
int a = 10;
int& r = a; // r 是 a 的引用

2. 初始化

  • 指针:指针在定义时可以不初始化,也可以指向 nullptr 或者某个变量的地址。
int* p; // 未初始化的指针(可能指向一个不确定的地址) 
int* p = nullptr; // 指针初始化为 null
  • 引用:引用必须在定义时初始化,并且一旦绑定到某个变量,就不能再更改引用的对象。
int& r = a; // 必须在定义时初始化

3. 空值

  • 指针:指针可以指向 nullptr,表示它不指向任何有效的内存地址。
int* p = nullptr; // p 不指向任何对象
  • 引用:引用必须引用一个有效的对象,不能是 nullptr 或空引用。

4. 重新赋值

  • 指针:指针可以在初始化后指向不同的对象或地址。
int a = 10; 
int b = 20; 
int* p = &a; // p 指向 a 
p = &b; // 现在 p 指向 b
  • 引用:引用在初始化后不能改变它所指向的对象,一旦绑定就不可更改。
int& r = a; // r 绑定到 a 
r = b; // 这里并不是改变引用,而是将 b 的值赋给 a

5. 内存管理

  • 指针:指针可以用于动态内存管理,可以通过 newdelete 操作符分配和释放内存。
int* p = new int(10); // 动态分配内存 
delete p; // 释放内存
  • 引用:引用不能用于直接管理内存,无法通过引用来分配或释放内存。

6. 使用场景

  • 指针:指针适用于需要动态管理内存、需要传递 nullptr 或者在函数中指向不同对象的情况。例如实现数据结构(如链表、树)时经常使用指针。
  • 引用:引用更常用于函数参数传递(尤其是传递大对象时)、返回值优化、以及表示某个对象的别名,以避免直接操作指针的复杂性。

7. 函数传参

  • 指针:可以通过指针参数来修改传入的变量,也可以传入 nullptr 表示无效的输入。
void modify(int* p) 
{ 
    if (p) 
    { 
        *p = 20; 
    } 
}
  • 引用:引用参数允许函数直接操作传入的变量,但不能接受 nullptr
void modify(int& r) 
{ 
    r = 20; 
}

8. 数组与指针的关系

  • 指针:数组名本质上是指向数组首元素的指针,可以通过指针算术来访问数组元素。
int arr[] = {1, 2, 3}; 
int* p = arr; // p 指向 arr[0] 
int x = *(p + 1); // 访问 arr[1]
  • 引用:引用不能用于指向整个数组,但可以引用数组中的单个元素或整个数组。
int (&arrRef)[3] = arr; // 引用整个数组 
int& elemRef = arr[1]; // 引用单个元素

指针和引用在 C++ 中各有其用途。指针灵活性高,可用于复杂的内存管理和数据结构操作,但也更容易出错。引用则更安全,使用起来更简单,但功能较为有限,通常用于函数参数传递和返回值的场景。在使用时,应根据具体需求选择合适的工具。

struct和union的区别

在C++中,‌structunion都是用来定义自定义数据类型的关键字,‌但它们在内存管理和数据使用上有显著的区别:‌

1. 内存管理:‌

  • struct中的每个成员都占用独立的内存空间,‌因此可以同时存在,‌并且各个成员之间的内存地址是连续的。‌struct可以包含函数成员,‌这些函数成员可以对结构体的数据进行操作。‌
  • union中的所有成员共享同一块内存空间。‌union的成员不能同时存在,‌只能有一个成员被使用。‌union的大小由其中最大的成员决定,‌并且不能包含函数成员。‌

2. 数据使用

  • struct适用于需要同时存储不同类型的数据的情况,‌每个成员占用独立的内存空间,‌因此可以对结构体的不同成员赋值而互不影响。‌
  • union适用于节省内存空间的情况,‌因为不同成员共享同一块内存。‌对union的不同成员赋值将会对其他成员重写,‌原来成员的值就不存在了。

3. 实例说明:‌

  • struct可以看作是一些相互关联的元素的集合,‌它们在内存中的存放有先后顺序,‌并且每个元素都有自己的内存空间。‌例如,‌如果定义了一个包含intchar, 和另一个自定义类型的成员的struct,‌那么这个struct的大小至少为这些成员大小的总和(‌考虑到可能的内存对齐)‌。‌
  • union的所有元素共享同一内存单元,‌分配给union的内存大小由类型最大的元素大小来确定。‌例如,‌如果一个union包含了intdouble, 和char的成员,‌那么union的大小将由double的大小决定,‌因为它是最大的成员。‌

总的来说,‌选择使用struct还是union取决于你的具体需求:‌如果你需要同时存储多个不相关的值,‌并且这些值需要独立存在,‌那么应该使用struct;‌如果你希望节省内存空间,‌并且可以接受在任何时候只有一个值存在的情况,‌那么应该使用union

#define和const的区别

C++中的#define和const用于定义常量,‌但它们在编译处理、‌类型、‌内存占用、‌调试能力等方面存在显著区别。‌

1. 编译处理方式:‌

  • #define宏定义是在预编译阶段展开的,‌而const常量是在编译运行阶段才展开的。‌这意味着#define宏定义可能会因为多次替换而导致内存损耗,‌而const常量则不会。‌

2. 类型和安全检查:‌

  • #define宏定义的常量没有类型,‌不做任何类型检查,‌仅仅是文本替换,‌这可能导致潜在的错误。‌
  • const定义的常量有具体的类型,‌在编译阶段会执行类型检查,‌这增加了代码的类型安全性。‌

3. 内存占用:‌

  • #define宏定义仅仅是字符替换,‌每次在程序中遇到都会进行替换,‌因此程序中会有很多宏的副本,‌产生内存损耗。‌
  • const常量在内存中只分配一次空间,‌无论是堆中还是栈中,‌这有助于节省空间并避免不必要的内存分配。‌

4. 调试能力:‌

  • 使用const常量比使用#define宏定义的常量更容易进行调试,‌因为它们有类型信息,‌并且可以在调试器中查看。‌

5. 作用域和重新定义:‌

  • #define宏定义的常量一旦定义,‌将在整个文件中可用,‌没有作用域限制。‌
  • const变量的作用域是局部的,‌只能在它们被声明的函数或代码块中访问。‌
  • 如果试图使用相同的#define宏名称定义一个不同的值,‌预处理器不会给出错误。‌但如果试图重新定义一个const变量,‌编译器会给出错误。‌

综上所述,‌虽然#define和const都可以用来定义常量,‌但const提供了更好的类型安全性、‌调试能力和内存管理效率,‌因此在现代C++编程中更推荐使用const来定义常量

强制类型转换

在C++中,强制类型转换是通过类型转换操作符来实现的。这些操作符可以把一个表达式转换成特定的类型。以下是几种常见的强制类型转换操作符:

  1. static_cast:用于非多态类型的转换。

  2. dynamic_cast:用于多态类型的转换,主要用于向下类型转换(从基类指向派生类的指针/引用),会检查转换的有效性,如果转换不安全,则无法进行转换。

  3. const_cast:用于去除 const 或 volatile 属性。

  4. reinterpret_cast:用于将任何指针类型转换成任何其他的指针类型,可能也包括指针与足够大的整数类型之间的转换。

int main() 
{
    double d = 3.14;
    // 使用 static_cast 将 double 转换为 int
    int i = static_cast<int>(d);
    // 使用 dynamic_cast 进行安全的向下转型
    class Base { virtual void dummy() {} };
    class Derived : public Base { int extra_data; };
    Base* ptr = new Derived;
    Derived* derived_ptr = dynamic_cast<Derived*>(ptr); // 正确转换
    // 使用 const_cast 去除 const 属性
    const int ci = 10;
    int* modifiable = const_cast<int*>(&ci);
    // 使用 reinterpret_cast 进行奇怪的转换
    int* p = new int(65);
    char* ch = reinterpret_cast<char*>(p);
 
    delete ptr;
    return 0;
}

左值和右值

在C++中,表达式分为左值(lvalue)和右值(rvalue)。简单来说:

  • 左值:指的是可以出现在赋值符号(=)左边的表达式,有确定的内存地址,代表内存中有值的表达式。

  • 右值:指的是可以出现在赋值符号(=)右边的表达式,有时也被称为临时值。

区分左值和右值的一个简单规则是:如果你可以取得对象的地址,那么这个对象就是左值,否则就是右值。

int a = 10; // 'a' 是左值,10 是右值
int* p = &a; // 正确,取址符号&a取得了a的地址,因此a是左值
 
int b = a + 1; // 正确,a作为左值参与加法运算
int* q = &(a + 1); // 错误,a + 1是右值,不能取其地址
 
int& r = a; // 正确,r是对a的引用,因此a是左值
int&& rr = 10; // 正确,10是右值,rr是对右值的右值引用

左值引用与右值引用

在C++中,有两种类型的引用:左值引用和右值引用。

  1. 左值引用:

    左值引用通常用来引用一个左值表达式(比如变量)。左值引用需要一个初始化的对象,你不能将其绑定到一个临时对象或者字面量。

  2. 右值引用:

    右值引用是C++11引入的新特性,用两个&&来声明,可以绑定到临时对象或者字面量。右值引用通常用于移动语义和性能优化。

int a = 10;
int& ref = a; // 正确,左值引用
// int& ref2 = 10; // 错误,不能将右值引用绑定到字面量

int a = 10;
int&& ref = 10; // 正确,右值引用
int&& ref2 = std::move(a); // 正确,右值引用可以绑定到临时对象

构造函数和析构函数的执行顺序

在C++中,构造函数和析构函数遵循以下执行顺序:

  1. 在创建基类对象时,基类的构造函数先执行;

  2. 然后是派生类的构造函数执行;

  3. 在销毁对象时,派生类的析构函数首先执行;

  4. 然后基类的析构函数执行。

#include <iostream>
 
class Base 
{
public:
    Base() { std::cout << "Base constructor called\n"; }
    ~Base() { std::cout << "Base destructor called\n"; }
};
 
class Derived : public Base 
{
public:
    Derived() { std::cout << "Derived constructor called\n"; }
    ~Derived() { std::cout << "Derived destructor called\n"; }
};
 
int main() 
{
    Derived d;
    // 输出顺序将是:
    // Base constructor called
    // Derived constructor called
    // (当d离开作用域或被销毁时)
    // Derived destructor called
    // Base destructor called
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值