在面试中,我常问的c++问题

一、简单问题

1.什么是基于对象设计?面向对象设计?请简要描述

基于对象设计(Object-Based Design)和面向对象设计(Object-Oriented Design, OOD)是两种常见的软件设计范式,它们都侧重于以对象为中心的编程和系统设计,但有一些区别。

  1. 基于对象设计

    • 基于对象设计关注于定义、创建和使用对象,但不一定涉及继承和多态等高级面向对象的特性。
    • 在这种设计范式中,数据和相关的操作被封装为对象。
    • 它主要支持封装和数据隐藏,对象可以通过暴露的方法进行操作,但不一定要求对象之间的复杂关系(如继承)。
  2. 面向对象设计

    • 面向对象设计是基于对象设计的扩展,它包括了继承、多态以及封装等特性。
    • 这种设计方式不仅包含对象的定义和使用,还涉及对象之间的继承关系,可以创建类的层次结构。
    • 面向对象设计允许对象之间有更复杂的交互,通过继承实现代码的复用,通过多态允许同一操作适用于不同的对象。

面向对象设计是更全面的方法,它使得软件开发更加模块化,易于管理和扩展。基于对象设计可以视为面向对象设计的一个子集,使用了一些面向对象的技术和概念,但没有完全采用面向对象的所有特性。

2.c语言和c++的区别和联系?请简要描述

联系:
  1. 基础共享:C++基于C语言,它完全兼容大部分的C语言代码。C++被设计为C的一个超集,这意味着除了一些特例外,几乎所有的合法C代码都是合法的C++代码。
  2. 语法结构:C++保留了C语言的基本语法和表达式,如变量定义、基本类型和语句结构等。
  3. 编译原理:两者都是编译型语言,需要通过编译器将源代码转换为机器代码后才能执行。
区别:
  1. 面向对象编程(OOP)
    • C++ 引入了类和对象的概念,支持封装、继承和多态等面向对象的核心概念。
    • C 是一种过程式语言,不支持面向对象的编程。
  2. 标准库和模板
    • C++ 提供了丰富的标准模板库(STL),包括向量、列表、映射、算法等,这些都是面向对象设计的。
    • C 标准库主要包括函数库,如标准输入输出、字符串处理、数学函数等,没有内置的模板支持。
  3. 类型安全
    • C++ 提供更多的类型安全特性,比如强类型枚举、函数重载和模板等,这可以在编译时发现更多的错误。
    • C 在类型转换和操作上更加灵活,但这也可能导致安全问题。
  4. 异常处理
    • C++ 支持异常处理,可以使用trycatchthrow语句来处理运行时错误。
    • C 没有内置的异常处理机制,通常使用函数返回值来表示错误。
  5. 命名空间
    • C++ 提供了命名空间,这是一种避免名字冲突的有效机制。
    • C 没有命名空间的概念,通常使用前缀或其他约定来管理名称空间。

3.在c++中,头文件的防卫式声明的作用是什么?

#ifndef __COMPLEX__
#define __COMPLEX__
class complex{
...
}
#endif
  1. 防止重复定义:当多个源文件包含同一头文件时,或当头文件互相包含时,防卫式声明确保头文件的内容不会被编译器重复处理。这避免了变量、函数、类等重复定义的错误。
  2. 提高编译效率:通过防止头文件内容的重复编译,可以减少编译时间,提高整体编译效率。
  3. 增强代码的可读性和可维护性:清晰的防卫式声明使得头文件的管理更为容易,减少了因文件依赖和引入顺序问题导致的编译错误。

4.在c++中,怎么理解声明和定义的概念?请举例说明

声明(Declaration)

声明是向编译器介绍名字(如变量、函数、类等)及其类型的语句,但不分配存储空间(除了外部变量的声明)。声明的主要目的是告知编译器某个标识符的存在和类型,使得在其他地方可以正确地引用它。

示例:

extern int x;  // 声明一个变量x,但不定义它
void foo();  // 声明一个函数foo,但不定义它
class Bar;  // 前向声明一个类Bar
定义(Definition)

定义是为程序的实体(如变量、函数、类)分配存储空间,或者提供实体的具体实现。定义不仅声明了实体,同时也初始化了它,这意味着编译器和链接器会在内存中为其分配空间。

示例:

int x;  // 定义一个变量x,并为其分配内存
void foo() {  // 定义函数foo,并提供具体的实现
    // 函数体
}
class Bar {  // 定义类Bar
public:
    int b;
    void method() {}  // 定义方法
};
声明 vs 定义
  • 声明可以多次出现,但一个程序实体的定义只能有一次(除了内联函数、类成员函数和模板的特殊规则)。
  • 如果声明一个变量为extern而没有初始化,这就是声明而非定义。例如,extern int x;仅仅告诉编译器变量x存在于某处,而不在当前位置创建它。
头文件中的声明和定义
  • 函数和类的声明通常放在头文件中,以便在多个源文件中使用。
  • 定义(如全局变量的定义和函数的实现)通常放在源文件中。将定义放在头文件中可能导致多重定义的链接错误,除非它们是内联的。

使用头文件的正确方式示例

// file: foo.h
#ifndef FOO_H
#define FOO_H

void foo();  // 声明函数

#endif

// file: foo.cpp
#include "foo.h"

void foo() {  // 定义函数
    // 实现
}

这种方式确保了函数foo的声明在任何需要使用它的文件中都可见,而定义只在一个源文件中提供,从而避免了链接时的多重定义问题。

5.在c++中,inline函数有什么作用?inline函数的规则是什么?

inline 函数的作用:
  1. 减少函数调用的开销:传统的函数调用涉及到跳转、传参、保存和恢复寄存器等开销。inline 函数通过将函数体嵌入到调用点,减少了这些开销。
  2. 提高程序运行效率:尤其是对于小型的、频繁调用的函数,使用 inline 可以显著提升性能。
inline 函数的规则和注意事项:
  1. 隐式和显式:成员函数在类定义中直接定义时默认是 inline 的。此外,可以显式地在函数声明前添加 inline 关键字。
  2. 编译器的自由裁量权:虽然 inline 关键字是一个向编译器的建议,但编译器可以选择忽略这个建议。特别是对于较大或复杂的函数,编译器可能决定不进行内联。
  3. 定义的可见性inline 函数的定义必须对每个调用它的翻译单元(translation unit)可见,通常意味着 inline 函数的定义应该在头文件中。
  4. 一致性:如果一个 inline 函数在多个文件中使用,它的定义在所有地方必须完全相同。
  5. 递归和虚函数:递归函数通常不适合做为 inline 函数,因为它们的调用成本高,且不容易内联。对于虚函数,除非在编译时可以确定调用的具体版本,否则通常不会内联。

6.在c++中,简单说明一下class的访问级别?public、privated等

  1. public
  • 定义:在 public 访问级别下的成员可以在任何地方被访问,无论是类的内部还是外部。
  • 使用场景:通常用于定义类的接口部分,即那些需要被外部使用的函数和数据成员。
  1. private
  • 定义private 访问级别的成员只能被其自身的类中的函数访问。
  • 使用场景:通常用于隐藏类的实现细节和内部状态,只允许类自己的成员函数和友元函数访问。
  1. protected
  • 定义protected 访问级别的成员可以被其自身类及继承该类的子类中的函数访问。
  • 使用场景:主要用于继承情况,当你希望子类能够访问和修改基类中的成员,但这些成员对外部世界仍然是隐藏的。

7.在编写c++构造函数一般关注哪些问题?

  1. 初始化所有成员变量

    • 构造函数的主要目的是初始化对象的所有成员变量。使用初始化列表是一种更有效的初始化方式,因为它可以直接调用成员的构造函数,避免了先默认构造再赋值的过程。
  2. 避免在构造函数中调用虚函数

    • 在构造函数中调用虚函数是不安全的,因为这类调用不会执行派生类中的重写版本,只会执行当前构造函数所在类层次的版本。这是因为在构造函数执行期间,对象的派生类部分尚未完全构造。
  3. 处理资源分配失败的情况

    • 如果构造函数中有资源分配(如动态内存、文件句柄等),需要确保在资源分配失败时能够正确处理异常或错误,防止资源泄漏。
  4. 考虑异常安全性

    • 构造函数应当是异常安全的,尤其是在涉及资源管理时。如果一个构造函数创建了一些资源后抛出异常,应该确保在抛出异常前释放那些已经分配的资源。
  5. 避免构造函数过于复杂

    • 构造函数应该尽量保持简单,只完成对象状态的初始化。避免在构造函数中执行复杂的逻辑或耗时的操作。
  6. 委托构造函数

    • C++11 引入了构造函数的委托功能,允许一个构造函数调用同一类的另一个构造函数。这有助于减少代码重复和提高代码清晰度。
  7. 默认构造函数的需求和影响

    • 如果类中定义了其他构造函数,编译器不会自动生成默认构造函数。如果类的对象可能会在没有任何参数的情况下被创建,应该显式定义一个无参数的构造函数。
  8. 使用显式关键字

    • 如果构造函数只接受一个参数,它可能会被用作隐式类型转换。在不期望这种行为时,可以用 explicit 关键字标记构造函数,防止隐式类型转换。

通过遵循这些指导原则,可以创建健壯且有效的构造函数,确保类的对象在创建时能够达到预期的状态和行为。

8.什么是stack(栈)?heap(堆)?从内存的角度回答

栈:
  1. 自动管理:栈由编译器自动管理,程序员无需手动控制内存的分配和释放。当函数被调用时,其局部变量和参数会被推入栈中;当函数返回时,这些变量会被自动清除。
  2. 速度快:由于栈是严格局部化的数据结构,内存访问模式高度可预测,这使得栈的数据访问非常快速。
  3. 大小限制:栈的大小通常在程序启动时被确定,且相对较小。这限制了栈上可存储数据的总量。
  4. 适用场景:适合存储函数调用的临时数据,如局部变量和调用记录。
堆:
  1. 动态内存管理:内存的分配和释放需要由程序员或运行时库手动管理,例如在C++中使用 newdelete,在C中使用 mallocfree
  2. 灵活性:堆允许动态地分配大量内存,只受限于系统的可用内存,这为存储大型数据结构和进行大规模数据处理提供了可能。
  3. 访问速度较慢:与栈相比,堆的内存访问速度较慢,因为堆的内存分配和管理更为复杂,可能涉及到内存碎片和搜索空闲内存块的问题。
  4. 内存碎片:频繁地在堆上分配和释放内存可能导致内存碎片,这是堆管理中常见的问题,可能会降低程序性能。

9.在c++中,什么是namespace?什么作用?如何使用?

在C++中,namespace 是一个用于定义一定范围内的名字空间的关键字,它帮助组织代码并防止命名冲突。使用命名空间可以更好地管理程序中的名称,尤其是在大型项目或多人合作的项目中。

作用:
  1. 防止命名冲突:命名空间允许开发者在不同的命名空间中定义相同的名字,这些名字彼此之间不会产生冲突。这是在使用多个库时特别有用的,因为不同库中可能有相同的函数或类名。

  2. 组织代码:命名空间可以将功能相似的类和函数组织在一起,便于管理和维护。

  3. 控制名称的可见性:命名空间内的名称不会自动进入全局命名空间,必须通过特定的语法来引用,这增加了程序的封装性和模块化。

如何使用:
  1. 定义命名空间

命名空间的定义使用关键字 namespace 后跟命名空间的名字,然后是命名空间的定义体:

namespace MyNamespace {
    int value;  // 在命名空间中定义变量
    void func() { // 在命名空间中定义函数
        // 函数体
    }
    class MyClass { // 在命名空间中定义类
    public:
        void method();
    };
}
  1. 使用命名空间中的成员

要使用命名空间中定义的成员,可以使用作用域解析运算符 ::

MyNamespace::func();  // 调用命名空间中的函数
MyNamespace::MyClass obj;  // 创建命名空间中的类的对象
obj.method();  // 调用对象的方法
  1. 使用 using 声明

如果不希望每次都写完整的命名空间前缀,可以使用 using 声明:

using MyNamespace::func;
func();  // 直接调用,不再需要前缀
  1. 使用 using 指令

你还可以将整个命名空间或特定成员引入到当前作用域:

using namespace MyNamespace;  // 引入整个命名空间
func();  // 现在可以直接调用func
MyClass obj;  // 同样可以直接创建MyClass对象
  1. 注意事项:
  • 使用 using namespace 指令虽然方便,但可能会导致命名冲突,特别是当引入多个命名空间时。
  • 建议在.cpp文件中使用 using namespace,而在头文件中避免使用,以防止命名冲突扩散到其他文件。

二、中级问题

1.关于c++中的const关键字,你有哪些了解?

在C++中,const 关键字用于定义常量,即一旦初始化后其值就不能改变。const 的使用增强了程序的安全性,提高了代码的可读性和维护性,并有助于优化编译器生成的代码。以下是 const 在C++中的几种主要用法:

1. 定义常量变量

使用 const 可以定义一个常量变量,表示这个变量的值在初始化后不能被修改。

const int maxCount = 100;  // maxCount 在定义后不能被修改
2. 定义常量指针和指针常量

const 可以用于指针的定义,涉及到指针指向的数据是否可以被修改,以及指针本身是否可以指向别处。

  • 常量指针(Pointer to const):指针指向的数据不可修改,但指针本身可以改变。
const int* ptr = &maxCount;  // 不能通过 ptr 修改 *ptr 的值,但可以修改 ptr 的指向
  • 指针常量(Const pointer):指针本身的指向不能修改,但可以通过指针修改其指向的数据。
int value = 10;
int* const ptr = &value;  // ptr 不能指向其他地址,但 *ptr 可以修改
  • 常量指针常量:指针本身和其指向的数据都不能被修改。
const int* const ptr = &maxCount;  // ptr 不能指向其他地址,同时 *ptr 也不能被修改
3. 常量成员函数

在类的成员函数后添加 const 关键字表示该函数不会修改类的任何成员变量(除了那些被标记为 mutable 的成员)。

class MyClass {
public:
    int getValue() const { return value; }  // 这个函数保证不会修改任何成员变量
private:
    int value;
};
4. 常量引用

常量引用,即引用的目标对象不能通过这个引用被修改。这常用于函数参数,以确保传入的参数在函数内不被改变,同时可以避免复制带来的开销。

void print(const std::string& str) {
    std::cout << str;  // str 作为引用传入,保证不被修改
}

2.关于c++中函数的参数传递有哪几种方式?什么情况下使用?

1. 传值(Pass by Value)
  • 方式:通过值传递时,函数收到的参数是原始数据的一个副本。在函数内部对参数的任何修改都不会影响原始数据。
  • 使用情况:适用于传递小型数据结构(如内置数据类型或小对象)。它简单且安全,不会影响原始数据。
void increment(int x) {
    x = x + 1;  // 只修改局部副本
}
2. 传引用(Pass by Reference)
  • 方式:通过引用传递时,函数接收的是原始数据的引用(不是副本)。这意味着在函数内部对参数的任何修改都会直接影响到原始数据。
  • 使用情况:适用于需要修改传入数据的场景,或者传递大型数据结构而不想产生复制的开销时。此方式可以提高效率,但需要注意可能会改变原始数据。
void increment(int& x) {
    x = x + 1;  // 直接修改原始数据
}
3. 传常量引用(Pass by Const Reference)
  • 方式:通过常量引用传递时,函数接收的是原始数据的引用,但是这个引用是常量,不能通过它修改数据。
  • 使用情况:适用于传递大型数据结构时,不想产生复制的开销,同时又不需要在函数内部修改数据。这是一种非常常用的参数传递方式,尤其在处理自定义类对象或复杂数据结构时。
void print(const std::string& str) {
    std::cout << str;  // 读取数据,但不进行修改
}
4. 传指针(Pass by Pointer)
  • 方式:通过指针传递时,函数接收的是一个指向原始数据的指针。类似于引用,通过指针可以修改原始数据,但也可以传递nullptr作为参数。
  • 使用情况:适用于传递可选的数据或当函数需要明确是否传递了有效数据。此外,传指针可以改变指向的地址,这在某些特定的数据结构操作中非常有用。
void increment(int* x) {
    if (x) {
        *x = *x + 1;  // 通过指针修改数据
    }
}

3.关于c++中函数返回值传递有哪几种方式?什么情况下使用?

1. 返回值传递(Return by Value)
  • 方式:函数通过值返回数据,意味着返回的是数据的副本。当函数结束时,返回值会被复制到接收变量中。
  • 使用情况:适用于返回基本数据类型(如 int, double),小型结构体或类。这种方法简单且安全,因为返回的副本与函数内部的原始数据是隔离的。
int add(int x, int y) {
    return x + y;
}
2. 返回引用(Return by Reference)
  • 方式:函数返回一个引用,即返回数据的别名。这意味着任何对返回值的修改都将影响原始数据。
  • 使用情况:适用于返回对象属性或全局变量,尤其是在需要避免数据复制且不关心数据封装的情况下。必须注意,返回的引用必须指向持久存储的对象,而非局部自动变量。
int& getMax(int& a, int& b) {
    return (a > b) ? a : b;
}
3. 返回常量引用(Return by Const Reference)
  • 方式:类似返回引用,但返回的是常量引用,防止返回的数据被修改。
  • 使用情况:当你希望避免数据复制,同时又不希望调用者修改返回的数据时使用。这种方式常见于返回类中的成员变量,而不希望外部代码修改它们。
const std::string& Person::getName() const {
    return name;
}
4. 返回指针(Return by Pointer)
  • 方式:函数通过指针返回数据。这允许返回空指针(nullptr),用于表示特殊情况或错误。
  • 使用情况:当函数需要返回动态分配的内存或者可选的返回值时使用。返回指针使调用者负责任何必要的内存管理。
int* find(int value, int* array, int size) {
    for (int i = 0; i < size; ++i) {
        if (array[i] == value) {
            return &array[i];  // 返回指向找到的元素的指针
        }
    }
    return nullptr;  // 没找到
}
5. 返回移动语义(Return by Move)
  • 方式:在C++11及之后的版本中,函数可以返回一个临时对象,并利用移动语义避免复制开销。
  • 使用情况:当函数需要返回复杂对象或拥有资源的对象时,利用移动构造函数和std::move可以大幅提高性能。
std::vector<int> getLargeVector() {
    std::vector<int> largeVector(1000, 42);
    return largeVector;  // C++11 及更高版本中,使用移动语义避免复制
}

4.什么是友元函数?它有什么作用?

在C++中,友元函数是一种特殊的函数,它虽然不是某个类的成员函数,但可以访问该类的所有私有(private)和保护(protected)成员。友元函数提供了一种在保持封装特性的同时,允许外部函数访问类的内部信息的机制。

作用:
  1. 访问私有成员:友元函数可以访问类的私有和保护成员,这使得它可以执行那些需要访问类内部数据的操作,而无需通过类的公共接口。

  2. 增强灵活性:有时,某些功能不方便或不合适作为类的成员实现,使用友元函数可以在不破坏类封装性的前提下,实现这些功能。

  3. 实现运算符重载:友元函数常用于实现需要访问类私有数据的运算符重载,例如重载输入输出运算符(<<>>)。

使用方式:

要声明一个友元函数,你需要在类定义内部使用 friend 关键字,后跟友元函数的原型声明。友元函数可以是一个普通的非成员函数,一个其他类的成员函数,或者是另一个类本身(这时整个类及其所有成员函数都成为友元)。

示例代码:
#include <iostream>

class Box {
private:
    double width;

public:
    Box(double wid) : width(wid) {}
    
    // 声明一个非成员函数为友元
    friend void printWidth(const Box& b);
};

// 友元函数定义
void printWidth(const Box& b) {
    // 直接访问私有成员
    std::cout << "Width of box: " << b.width << std::endl;
}

int main() {
    Box box(10.0);
    printWidth(box);  // 访问Box类的私有数据
    return 0;
}

在这个例子中,printWidth 函数虽然不是 Box 类的成员,但因为被声明为友元,它可以访问 Box 类的私有成员 width

5.以下代码语法是正确还是错误?为什么?(待验证)

class complex
{
public:
	complex (double r = 0, double i = 0)
		: re (r), im (i) 
	{ }
	int func(const complex& param) { return param.re + param.im; }
private:
	double re, im;
};

正确的,相同class的各个对象具有互为友元属性。

6.什么是拷贝构造和拷贝赋值函数?它们解决什么问题?可举例说明

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,当一个对象以同类型的另一个对象进行初始化时被调用。其一般形式为:

ClassName(const ClassName& other);
  • 解决的问题:默认的拷贝构造函数是浅拷贝,它只复制值或指针,而不复制指针所指向的数据。如果类包含指向动态分配内存的指针,浅拷贝可能导致多个对象指向同一内存区域,这在析构时会引发问题如重复释放同一内存。拷贝构造函数使得每个对象可以拥有指向自己独立内存的指针。
示例
class String {
public:
    char* data;

    String(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // 拷贝构造函数
    String(const String& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    ~String() {
        delete[] data;
    }
};

在这个例子中,拷贝构造函数确保了每次对象被复制时,都会为字符串数据分配新的内存,从而避免了析构时的双重删除问题。

拷贝赋值函数

拷贝赋值函数被用于已经初始化的对象间的赋值。其一般形式为:

ClassName& operator=(const ClassName& other);
  • 解决的问题:和拷贝构造函数一样,拷贝赋值函数处理对象间的深拷贝问题,确保当一个对象被赋予另一个对象的状态时,任何需要深拷贝的资源(如动态内存)都被正确管理。同时,它还需要处理自赋值的情况并优化资源使用(如重用已分配的内存)。
示例
class String {
public:
    char* data;

    String(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // 拷贝赋值运算符
    String& operator=(const String& other) {
        if (this != &other) { // 自赋值检查
            delete[] data; // 释放现有内存
            data = new char[strlen(other.data) + 1];
            strcpy(data, other.data);
        }
        return *this;
    }

    ~String() {
        delete[] data;
    }
};

在这个例子中,拷贝赋值函数首先检查自赋值,然后释放现有内存,最后从源对象复制数据。这保证了在赋值时资源得到正确的管理。

7.以下代码语法是正确还是错误?为什么?

String* p = new String[3];
delete p;

错误的,应该是delete [] p;

在 C++ 中,使用 new[] 运算符分配的数组内存应当通过 delete[] 运算符来释放,而不是通过 delete 运算符。这是因为 new[]delete[] 是为处理数组分配和释放专门设计的,而 newdelete 是为单个对象设计的。

具体来说,使用 delete 而非 delete[] 来释放通过 new[] 创建的数组内存会导致以下问题:

  1. 不完整的析构

    • 使用 delete 释放数组只会调用数组第一个元素的析构函数,而不会为数组中的其他元素调用析构函数。这会导致资源泄漏,特别是如果数组元素是复杂对象且拥有自己的资源(如动态内存、文件句柄等)时。
  2. 内存泄漏

    • 由于未正确调用所有元素的析构函数,与这些元素相关的资源可能不会被释放,导致内存泄漏。
  3. 运行时错误

    • 在某些实现中,new[]delete[] 可能会在分配的内存块中存储元素数量等额外信息,以确保正确地构造和析构每个元素。使用 delete 代替 delete[] 可能会导致程序无法正确处理这些信息,从而引发运行时错误,如崩溃或行为异常。

因此,正确的代码应该使用 delete[] 来释放数组:

String* p = new String[3];
delete[] p; // 正确使用 delete[]

8.关于c++中的static关键字,你有哪些了解?

1. 静态成员变量

当在类定义中将成员变量声明为 static 时,这意味着无论创建多少对象,该类的所有实例都将共享这个变量。静态成员变量不属于任何单个对象,而是属于类本身。它在程序开始时被初始化,通常在对象创建之前。

使用示例

class Account {
public:
    static double interestRate;  // 静态成员变量
    static void updateInterestRate(double rate) {  // 静态成员函数
        interestRate = rate;
    }
};

double Account::interestRate = 3.5;

在这个例子中,interestRate 是所有 Account 对象共享的。

2. 静态成员函数

静态成员函数与静态成员变量类似,它们也是属于类本身而不是属于任何特定的对象。这意味着静态成员函数只能访问静态成员变量或调用其他静态成员函数,它们无法访问类的非静态成员。

使用示例

// 使用上面的 Account 类
Account::updateInterestRate(4.5);  // 调用静态成员函数修改利率
3. 静态局部变量

在函数内部声明的静态局部变量与普通局部变量不同,它在第一次调用该函数时被初始化,并且其值在函数调用结束后仍然存在。在下次调用该函数时,变量保持上次调用结束时的状态。

使用示例

void function() {
    static int counter = 0;  // 静态局部变量
    counter++;
    std::cout << "function called " << counter << " times" << std::endl;
}

每次调用 function 时,counter 都会递增,且不会在函数调用结束时重置。

4. 静态全局变量

在全局或命名空间范围内使用 static 声明变量或函数,可以限制其链接范围只在定义它的文件内。这意味着该变量或函数在其他文件中是不可见的,即使使用了外部链接也是如此。

使用示例

// 在 file1.cpp 中
static int internalVariable = 0;  // 静态全局变量

// 在 file2.cpp 中
// internalVariable 在这里不可见

9.什么是函数模版?类模板?请举例说明?

函数模板

函数模板允许创建一种模式,使得函数可以接受任意类型的参数。当你编写函数时,你可能不知道未来会用什么数据类型调用该函数。使用函数模板可以让你定义一个函数原型,通过该原型可以用来处理不同的数据类型。

示例:

template <typename T>
T findMax(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    // 使用整数
    std::cout << findMax(5, 10) << std::endl;  // 输出: 10

    // 使用浮点数
    std::cout << findMax(5.5, 2.2) << std::endl;  // 输出: 5.5

    // 使用字符串
    std::cout << findMax(std::string("apple"), std::string("banana")) << std::endl;  // 输出: banana
}

在这个例子中,findMax 函数模板可以用于任何支持比较运算符 > 的数据类型。

类模板

类模板与函数模板类似,允许定义一种框架,可以用任何类型作为类的成员数据类型。这对于数据结构(如列表、队列、栈等)特别有用,因为这些结构可以存储任何类型的数据。

示例:

template <typename T>
class Stack {
private:
    std::vector<T> elements;

public:
    void push(const T& elem) {
        elements.push_back(elem);
    }

    T pop() {
        if (elements.empty()) {
            throw std::out_of_range("Stack<>::pop(): empty stack");
        }
        T elem = elements.back();
        elements.pop_back();
        return elem;
    }

    bool isEmpty() const {
        return elements.empty();
    }
};

int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    std::cout << intStack.pop() << std::endl;  // 输出: 2

    Stack<std::string> stringStack;
    stringStack.push("hello");
    stringStack.push("world");
    std::cout << stringStack.pop() << std::endl;  // 输出: world
}

10.举例说明c++中类之间的复合、委托、继承关系?

1. 复合(Composition)

复合意味着一个类包含另一个类的对象作为其成员变量。这种关系通常被称为“拥有”关系(has-a)。复合表明,一个类的对象完全拥有另一个类的对象。

示例:

class Engine {
public:
    void start() {
        std::cout << "Engine started." << std::endl;
    }
};

class Car {
private:
    Engine engine; // Car "has-a" Engine

public:
    void start() {
        engine.start();
        std::cout << "Car started." << std::endl;
    }
};

int main() {
    Car myCar;
    myCar.start();  // 启动车辆时,先启动引擎
}

在这个例子中,Car 类通过包含一个 Engine 类的对象来实现复合。当车辆启动时,它首先调用其引擎的启动方法。

2. 委托(Delegation)

委托是一种设计模式,一个类通过拥有另一个类的对象来实现部分功能,但与复合不同的是,它更强调行为的委托而不是数据的拥有。

示例:

class Worker {
public:
    void doWork() {
        std::cout << "Work done." << std::endl;
    }
};

class Manager {
private:
    Worker worker;  // Manager delegates work to Worker

public:
    void manage() {
        std::cout << "Manager asks worker to perform a task." << std::endl;
        worker.doWork();  // Delegation
    }
};

int main() {
    Manager manager;
    manager.manage();
}

在这个例子中,Manager 类不直接完成工作,而是委托 Worker 类的对象完成工作。这展示了委托的使用,Manager 类依赖 Worker 类来执行具体任务。

3. 继承(Inheritance)

继承是面向对象编程中的一个核心概念,它允许一个类继承另一个类的属性和方法。这种关系通常被称为“是一个”关系(is-a)。

示例:

class Vehicle {
public:
    void start() {
        std::cout << "Vehicle started." << std::endl;
    }
};

class Car : public Vehicle {  // Car "is-a" Vehicle
    // Inherits start method from Vehicle
};

int main() {
    Car myCar;
    myCar.start();  // Car inherits the start method from Vehicle
}

在这个例子中,Car 类继承自 Vehicle 类。这意味着每辆车都是一种交通工具,且 Car 类继承了 Vehicle 类的所有公有成员和方法。

11.简单介绍一下非虚函数、虚函数、纯虚函数,以及使用场景

1. 非虚函数

非虚函数是最普通的成员函数,不使用任何虚拟机制。它们在类的继承体系中的行为是静态的,也就是说,调用哪个函数是在编译时决定的。

使用场景

  • 当你知道函数不需要在派生类中被重写时,应使用非虚函数。
  • 用于实现类的功能,其中不需要多态行为的函数。

示例

class Animal {
public:
    void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
};
2. 虚函数

虚函数允许在派生类中被重写,它们使得基类可以定义一个接口,由派生类提供具体的实现。当通过基类指针或引用调用虚函数时,会发生动态绑定(运行时多态),调用的是对象实际类型的函数版本。

使用场景

  • 当你希望在派生类中可以修改函数行为时,应使用虚函数。
  • 用于实现多态性,即允许通过基类的指针或引用来调用派生类的方法。

示例

class Animal {
public:
    virtual void speak() {
        std::cout << "Some animal sound!" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Bark!" << std::endl;
    }
};
3. 纯虚函数

纯虚函数是一种特殊的虚函数,它在基类中没有具体的实现,只提供接口的声明。一个包含纯虚函数的类称为抽象类,不能实例化。纯虚函数必须在任何非抽象的派生类中得到实现。

使用场景

  • 当你想创建一个接口,强制派生类实现特定的函数时,应使用纯虚函数。
  • 用于定义一个强制派生类遵守的接口,这种技术是实现接口继承的基础。

示例

class Animal {
public:
    virtual void speak() = 0;  // 纯虚函数
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Bark!" << std::endl;
    }
};

在这个例子中,Animal 类是一个抽象基类,它定义了一个纯虚函数 speakDog 类继承自 Animal 并提供了 speak 函数的具体实现。

12.简单介绍一下c++中的深拷贝和浅拷贝?

浅拷贝(Shallow Copy)

浅拷贝是对象复制的最简单形式,它仅仅复制对象的非静态成员变量的值。如果成员变量是指针,浅拷贝只复制指针的值(即内存地址),而不复制指针所指向的数据。

特点

  • 快速且省内存,因为不涉及额外的内存分配。
  • 如果对象包含指向动态分配内存的指针,可能会导致多个对象指向同一块内存,从而引发诸如双重释放(double free)等问题。

示例

class Shallow {
public:
    int* data;
    Shallow(int d) {
        data = new int(d);
    }
    ~Shallow() {
        delete data;
    }
};

void func() {
    Shallow obj1(20);
    Shallow obj2 = obj1; // 浅拷贝发生
}

在这个例子中,obj2 的构造是通过浅拷贝 obj1 实现的,这意味着 obj1.dataobj2.data 指向同一内存地址。当 obj1obj2 被销毁时,同一内存位置会被尝试释放两次,导致运行时错误。

深拷贝(Deep Copy)

深拷贝不仅复制对象的非静态成员变量的值,还复制这些变量所指向的数据。如果对象有指针指向一些资源(如内存、文件等),深拷贝会在堆上创建这些资源的一个新副本,并将新对象的指针指向这些新分配的资源。

特点

  • 安全性更高,因为每个对象都有自己独立的资源副本。
  • 比浅拷贝耗费更多的时间和内存,因为需要复制所有的数据。

示例

class Deep {
public:
    int* data;
    Deep(int d) {
        data = new int(d);
    }
    Deep(const Deep& source) { // 实现深拷贝
        data = new int(*source.data);
    }
    ~Deep() {
        delete data;
    }
};

void func() {
    Deep obj1(20);
    Deep obj2 = obj1; // 深拷贝发生
}

在这个例子中,通过定义拷贝构造函数来实现深拷贝,确保每个 Deep 对象都有自己独立的内存副本。因此,当 obj1obj2 被销毁时,它们各自释放自己独立的内存,不会发生双重释放的问题。

  • 13
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白茶-清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值