C++面试题+部分代码例子

C++中各种特性

总目录

1.static

static 是一个关键字,它可以用于不同的场景,在 C++ 中是一个相当有用的关键字。下面是对其主要用途的简要概述:

  1. 修饰局部变量:将局部变量声明为 static,使其在函数调用之间保持其值。因此,该变量的生命周期与程序的生命周期相同,而不是只在函数调用期间存在
#include <iostream>

void increment() {
    static int count = 0; // 静态变量,在函数调用结束后不会被销毁,保持其值
    count++;
    std::cout << "Count is: " << count << std::endl;
}

int main() {
    increment(); // 输出:Count is: 1
    increment(); // 输出:Count is: 2
    increment(); // 输出:Count is: 3
    return 0;
}

  1. 修饰全局变量:当 static 修饰全局变量时,只有在声明该变量的文件中才可以使用该变量。这样可以在多个文件中使用具有相同名称的全局变量,而不会出现命名冲突的问题。
// file1.cpp
#include <iostream>

static int globalVariable = 10;

void function1() {
    std::cout << "globalVariable in file1.cpp: " << globalVariable << std::endl;
}

// file2.cpp
#include <iostream>

extern int globalVariable; // 引用file1.cpp中的globalVariable

void function2() {
    std::cout << "globalVariable in file2.cpp: " << globalVariable << std::endl;
}

int main() {
    function1(); // 输出:globalVariable in file1.cpp: 10
    function2(); // 输出:globalVariable in file2.cpp: 10
    return 0;
}

在这个示例中,globalVariable被声明为静态,因此只能在file1.cpp中访问。在file2.cpp中,通过extern关键字引用了globalVariable,以便在file2.cpp中使用它。

  1. 修饰函数:当 static 修饰函数时,将其限制为该文件内部的函数,即该函数只能在声明它的文件中调用,不能在其他文件中调用。
// file1.cpp
#include <iostream>

static void foo() {
    std::cout << "foo() in file1.cpp" << std::endl;
}

// file2.cpp
#include <iostream>

void bar() {
    std::cout << "bar() in file2.cpp" << std::endl;
}

int main() {
    foo(); // 非法调用,会报错
    bar(); // 合法调用
    return 0;
}

在这个示例中,foo函数被声明为静态,因此只能在file1.cpp文件中调用。bar函数没有被声明为静态,因此可以在其他文件中调用。

  1. 修饰类成员变量:当 static 修饰类的成员变量时,这些变量将成为类本身的变量,而不是类的实例的变量。这些成员变量只有一个副本,供所有类的实例使用。因此,可以将这些变量用于类的所有实例之间的共享数据。
#include <iostream>

class MyClass {
public:
    static int staticVar;//定义静态类成员变量
    MyClass(int val) : value(val) {}
    void printValues() {
        std::cout << "value: " << value << ", staticVar: " << staticVar << std::endl;
    }

private:
    int value;
};

int MyClass::staticVar = 0; // 静态成员变量的定义和初始化

int main() {
    MyClass obj1(10);
    MyClass obj2(20);

    obj1.printValues(); // 输出:value: 10, staticVar: 0
    obj2.printValues(); // 输出:value: 20, staticVar: 0

    MyClass::staticVar = 5; // 直接通过类名访问静态成员变量

    obj1.printValues(); // 输出:value: 10, staticVar: 5
    obj2.printValues(); // 输出:value: 20, staticVar: 5

    return 0;
}

  1. 修饰类成员函数:当 static 修饰类的成员函数时,可以将其视为独立于类的实例的函数,即,不需要通过类的实例来调用。因此,可以使用类名来访问它们,而不必创建类的实例
#include <iostream>

class MyClass {
public:
    static void staticFunction() {
        std::cout << "Static function called" << std::endl;
    }

    void normalFunction() {
        std::cout << "Normal function called" << std::endl;
    }
};

int main() {
    MyClass::staticFunction(); // 直接调用静态函数,输出:Static function called

    MyClass obj;
    obj.normalFunction(); // 输出:Normal function called

    // MyClass::normalFunction(); // 错误,静态函数无法访问非静态成员
    // obj.staticFunction(); // 错误,对象无法访问静态函数

    return 0;
}

2.const

在C++中,const关键字用于声明常量,它可以用于不同的上下文,包括:

  1. 常量变量(Constant Variables):声明变量时,使用const关键字可以将其声明为常量,使得其值在初始化后不能被修改。例如:

    const int MAX_VALUE = 100;
    
  2. 常量指针(Constant Pointers):将指针声明为const可以防止修改指针所指向的变量,但不阻止修改指针本身。例如:

    int value = 10;
    const int *ptr = &value; // 指向常量的指针,不能通过ptr修改value的值
    
  3. 常量引用(Constant References):使用const修饰引用,可以创建对常量的引用,防止通过引用修改变量的值。例如:

    int value = 10;
    const int &ref = value; // 对value的常量引用,不能通过ref修改value的值
    
  4. 成员函数中的const(Const Member Functions):在类中声明成员函数时,如果函数不修改对象的状态,则应将其声明为const成员函数。这样做可以确保在const对象上调用该函数时不会修改对象的状态。例如:

    class MyClass {
    public:
        int getValue() const { return value; } // const成员函数,不修改对象的状态
    private:
        int value;
    };
    

总的来说,const关键字用于声明常量,防止修改变量的值或者防止修改指针或引用所指向的变量,以及声明const成员函数以确保不修改对象的状态。

  1. 需要注意的是,const 在修饰变量、函数参数和函数返回值时,都可以使用在左侧或右侧,但有一些微妙的差别。例如:
    const int *p 和 int const *p 是等价的,它们都表示指向常量的指针。
    int * const p 表示指针是常量,即 p 不能指向其他地址,但 p 指向的地址中的值可以被修改。
    const int * const p 表示指向常量的常量指针,既不能修改指针的值,也不能修改指针所指向的地址中的值。

3.面向对象的三大特征,封装性、继承性和多态性:

  1. 封装性:将客观事物抽象成类,每个类对自身的数据和方法实行 protection(private, protected, public)。

  2. 继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。

  3. 多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。 这部分需要熟悉掌握原理虚函数,了解一些概念(静态多态、动态多态)等,面试时经常会问。

    说明:面向对象的三个特征是实现面向对象技术的关键,每一个特征的相关技术都非常的复杂,程序员应该多看、多练。

3.1.封装

在C++中,privateprotectedpublic是用于指定类成员的访问权限的三个访问修饰符。

  1. private

    • 私有成员只能被同一个类中的成员函数访问,无法被类的对象或者其他类的成员函数直接访问。
    • 私有成员对于类的用户来说是不可见的,因此无法直接访问或者修改。
  2. protected

    • 受保护成员可以被同一个类中的成员函数以及派生类中的成员函数访问。
    • 受保护成员对于类的用户来说是不可见的,但可以在派生类中被访问。
  3. public

    • 公有成员可以被任何函数访问,包括同一个类中的成员函数、派生类中的成员函数以及类的用户代码。
    • 公有成员对于类的用户来说是可见的,并且可以直接访问。

下面是一个示例说明它们之间的区别:

class MyClass {
private:
    int privateMember;

protected:
    int protectedMember;

public:
    int publicMember;

    void accessPrivate() {
        privateMember = 10; // 合法,访问私有成员
    }

    void accessProtected() {
        protectedMember = 20; // 合法,访问受保护成员
    }

    void accessPublic() {
        publicMember = 30; // 合法,访问公有成员
    }
};

class DerivedClass : public MyClass {
public:
    void accessProtectedFromDerived() {
        protectedMember = 40; // 合法,在派生类中访问受保护成员
    }
};

int main() {
    MyClass obj;
    obj.publicMember = 50; // 合法,访问公有成员

    // obj.privateMember = 60; // 错误,私有成员不可访问
    // obj.protectedMember = 70; // 错误,受保护成员不可访问

    DerivedClass derivedObj;
    // derivedObj.protectedMember = 80; // 错误,受保护成员不可访问

    return 0;
}

在这个示例中,privateMember是私有成员,在MyClass外部无法访问。protectedMember是受保护成员,可以在MyClass和其派生类中访问,但在类的外部是不可见的。publicMember是公有成员,可以在任何地方访问。

3.2.什么叫做多态性?在 C++种是如何实现多态的?

多态是指同样的消息被不同类型的对象接收时导致完全不同的行为,是对类的特定成员函数的再抽象。
C++支持重载多态,强制多态,包含多态和参数多态。 在基类中声明相应的函数为 virtual 型,然后在派生类中实现该函数,这样就可以通过基类指针调用派生类对象的函数,实现了运行时动态绑定,即多态的功能。

#include <iostream>
using namespace std;

class Shape {
   protected:
      int width, height;

   public:
      Shape( int a = 0, int b = 0) {
         width = a;
         height = b;
      }
      virtual int area() {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};

class Rectangle: public Shape {
   public:
      Rectangle( int a = 0, int b = 0):Shape(a, b) { }

      int area () {
         cout << "Rectangle class area :" <<endl;
         return (width * height);
      }
};

class Triangle: public Shape{
   public:
      Triangle( int a = 0, int b = 0):Shape(a, b) { }
      
      int area () {
         cout << "Triangle class area :" <<endl;
         return (width * height / 2);
      }
};

int main() {
   Shape *shape;
   Rectangle rec(10,7);
   Triangle tri(10,5);
   
   // 存储矩形的地址
   shape = &rec;
   // 调用矩形的求面积函数 area
   shape->area();
   // 存储三角形的地址
   shape = &tri;
   // 调用三角形的求面积函数 area
   shape->area();
   return 0;
}

在此示例程序中,Shape 是一个基类,Rectangle 和 Triangle 是两个派生类,它们都继承了 Shape 类的数据成员和成员函数。Shape 类中定义了 virtual 关键字的虚函数 area(),在派生类中对其进行了重写(即覆盖)。在 main() 函数中,首先创建了一个基类指针 shape,然后将其分别指向 Rectangle 和 Triangle 对象。在调用 shape->area() 函数时,程序在运行时动态地选择了哪个类的函数将被调用,因此使得程序具有了多态性。

3.2.为什么说”继承是 C++面向对象的主要特征之一”?请简要说明.?

继承是一种联结类的层次模型,层次结构的上层是最具有通用性的,而下层的部分,即后代具有特殊性。类可以从他的祖先那里继承方法和成员变量,也可以增加新的方法是适用于特殊的需要。如果没有继承,类就缺失了一块层次结构,代码重用和数据共享就贯彻不到底,有了继承,就会有抽象编程中的多态问题,只有从机制内部真正解决了多态表现问题,对象的数据封装,信息隐藏,代码重用等招式才能淋漓尽致的发挥出来。才称得上真正的面向对象编程。

4.C中的 malloc 和C++中的 new 有什么区别

  1. new、delete 是操作符,可以重载,只能在 C++中使用。
  2. malloc、free 是函数,可以覆盖,C、C++中都可以使用。
  3. new 可以调用对象的构造函数,对应的 delete 调用相应的析构函数。
  4. malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数
  5. new、delete 返回的是某种数据类型指针,malloc、free 返回的是 void 指针。

new创建对象会调用构造函数,delete删除对象会调用析构函数

5.析构函数

我们已经知道构造函数是在创建对象时,对其进行初始化。而析构函数与其相反,是在对象被删除前象由系统自动执行它做清理工作。作为一个类,可能有多个对象,每个对象生命结束时都要调用析构函数,且每个对象调用一次。

特点:
(1)无类型
(2)无返回值
(3)名字与类名相同
不带参数,不可重载,析构函数只有一个!
析构函数前“~” (取反符,表示逆构造函数)
作用:在对象被删除前做清理工作。

注意:对象的析构函数在对象被销毁前被调用,对象何时销毁也与其作用域相关。

例如,全局对象是在程序运行结束时销毁;
自动对象是在离开其作用域时销毁;
而动态对象是在使用delete运算符时销毁。

析构函数特别适用于当一个对象被动态分配内存空间,而在对象被销毁前希望释放它所占用的内存空间的时候。我们不会忽略初始化的重要性,却常常忽略清除的重要性,然而对销毁变量的内存清理是非常重要的。

例如,我们在堆中申请了一些内存,如果没有用完就释放,会造成内存泄露,会导致应用程序运行效率降低,甚至崩溃,不可掉以轻心。

而在c++中提供有析构函数,可以保证对象清除工作自动执行。
析构与构造的调用次序相反,即最先构造的最后被析构,最后构造的最先被析构。

6.比较值传递和引用传递

值传递:是指当发生函数调用时,给形参分配内存空间,并用实参来初始化形参(直接将实参的值传递给形参)。
这一过程是参数值的单向传递过程,一旦形参获得了值便与实参脱离关系,此后无论形参发生了怎样的改变,都不会影响到实参。

引用传递:将引用作为形参,在执行主调函数中的调用语句时,系统自动用实参来初始化形参。这样形参就成为实参的一个别名,对形参的任何操作也就直接作用于实参。

7.什么叫内联函数?它有哪些特点?

定义时使用关键字 inline 的函数叫做内联函数;编译器在编译时在调用处用函数体进行替换,节省了参数传递、控制转移等开销;
内联函数体内不能有循环语句和 switch 语句;
内联函数的定义必须出现在内联函数第一次被调用之前;对内联函数不能进行异常接口声明;

内联函数:

inline const string& longerStr(const string& str1, const string& str2)
{
	return str1.size() > str2.size() ? str1 : str2;
}

当我们试图打印输出调用结果时:

cout << longerStr(str1, str2) << endl;

编译器会自动把它展开为:

cout << (str1.size() > str2.size() ? str1 : str2) << endl;

这样就大大提高了运行效率。

8.构造函数和析构函数

构造函数:

1、构造函数概念
一个类的对象被创建的时候,编译系统对象分配内存空间,并自动调用该构造函数,由构造函数完成成员的初始化工作。因此,构造函数的核心作用就是,初始化对象的数据成员

2、构造函数的特点
(1)名字与类名相同,可以有参数,但是不能有返回值(连void也不行)。
(2)构造函数是在实例化对象时自动执行的,不需要手动调用。
(3)作用是对对象进行初始化工作,如给成员变量赋值等。
(4)如果定义类时没有写构造函数,系统会生成一个默认的无参构造函数,默认构造函数没有参数,不做任何工作。
(5)如果定义了构造函数,系统不再生成默认的无参构造函数.
(6)对象生成时构造函数自动调用,对象一旦生成,不能在其上再次执行构造函数
(7)一个类可以有多个构造函数,为重载关系。

3、构造函数的分类

  • 按参数种类分为:无参构造函数、有参构造函数、有默认参构造函数
  • 按类型分为:普通构造函数、拷贝构造函数(赋值构造函数)
//声明Time类 
class Time
{
public:  //成员函数共有部分 
	Time()  //定义构造成员函数,函数名与类名相同 
	{
		hour= 0;  //利用构造函数给对象中的数据成员都赋初值为0 
		minute= 0;
		sec= 0;
	}
	//成员函数的声明
	void set_time(); 
	void show_time(void);
private:  //类的私有数据部分 
	int hour;  //默认数据也是私有的 
	int minute;
	int sec; 
};

析构函数

我们已经知道构造函数是在创建对象时,对其进行初始化。而析构函数与其相反,是在对象被删除前象由系统自动执行它做清理工作。
作为一个类,可能有多个对象,每个对象生命结束时都要调用析构函数,且每个对象调用一次。

特点:

  • 无类型
  • 无返回值
  • 名字与类名相同

不带参数,不可重载,析构函数只有一个!
析构函数前“~” (取反符,表示逆构造函数)
作用:在对象被删除前做清理工作。

注意:对象的析构函数在对象被销毁前被调用,对象何时销毁也与其作用域相关。
例如,全局对象是在程序运行结束时销毁;
自动对象是在离开其作用域时销毁;
而动态对象是在使用delete运算符时销毁。

析构函数特别适用于当一个对象被动态分配内存空间,而在对象被销毁前希望释放它所占用的内存空间的时候。我们不会忽略初始化的重要性,却常常忽略清除的重要性,然而对销毁变量的内存清理是非常重要的。

例如,我们在堆中申请了一些内存,如果没有用完就释放,会造成内存泄露,会导致应用程序运行效率降低,甚至崩溃,不可掉以轻心。
而在c++中提供有析构函数,可以保证对象清除工作自动执行。
析构与构造的调用次序相反,即最先构造的最后被析构,最后构造的最先被析构。

9.虚函数、纯虚函数

虚函数:虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数,是C++中多态性的一个重要体现利用基类指针访问派生类中的虚函数,这种情况下采用的是动态绑定技术。

纯虚函数:纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”.纯虚函数不能实例化对象。

Shape 是一个抽象类,它定义了两个抽象方法 getArea 和 getPerimeter。这两个方法只是定义了接口,没有任何实现。
Rectangle 类继承自 Shape,并实现了父类的抽象方法,从而具体化了 Shape 接口。
class Shape {
public:
    virtual double getArea() = 0; // 纯虚函数,表示这是一个接口,需要子类来实现
    virtual double getPerimeter() = 0;
};

class Rectangle : public Shape {
public:
    Rectangle(double width, double height): m_width(width), m_height(height) {}
    virtual double getArea() override { return m_width * m_height; } // 实现父类的抽象方法
    virtual double getPerimeter() override { return 2 * (m_width + m_height); }
private:
    double m_width;
    double m_height;
};

抽象类的介绍
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
(1)抽象类的定义: 称带有至少有一个纯虚函数的类为抽象类。

(2)抽象类的作用: 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

(3)使用抽象类时注意:

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

抽象类是不能定义对象的。

9.1.总结

1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。

2、虚函数声明如下:virtual ReturnType FunctionName(Parameter) 虚函数必须实现,如果不实现,编译器将报错。虚函数可以重写,不重写则沿用自父类虚函数。

3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

4、实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。

5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。

6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。

纯虚函数的引入,是出于两个目的:

  • 为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
  • 为了效率,不是程序执行的效率,而是为了编码的效率。

9.2.虚函数、纯虚函数区别

假设有一个动物类 Animal,这个类有一个纯虚函数 makeSound,另外还有一个虚函数 eat。其定义如下:

 class Animal {
public:
    virtual void eat() { cout << "Animal is eating." << endl; }
    virtual void makeSound() = 0; // 纯虚函数
};

makeSound 函数是一个纯虚函数,它没有具体的实现代码,而是要求其子类必须重写(override)这个函数。
如果一个类中有一个或者多个纯虚函数,那这个类就是抽象类,不能被实例化(即不能创建该类的对象)。

例如,我们创建了一个 Cat 类和一个 Dog 类继承自 Animal 类,并重写 makeSound 函数,如下所示:

class Cat : public Animal {
public:
    void makeSound() { cout << "Meow" << endl; }
};

class Dog : public Animal {
public:
    void makeSound() { cout << "Woof" << endl; }
};

因为 Animal 类是抽象类,不能直接实例化,但是我们可以定义一个指向 Animal 类的指针或者引用,来指向它的子类 Cat 或者 Dog。例如:

int main() {
    Animal* pCat = new Cat();
    Animal* pDog = new Dog();

    pCat->makeSound(); // 输出 Meow
    pDog->makeSound(); // 输出 Woof

    delete pCat;
    delete pDog;

    return 0;
}

虚函数 eat 是一个普通的虚函数,它有默认的实现代码,但是它也可以被子类重写。如果子类不重写 eat 函数,就会沿用父类的默认实现。例如:

class Cat : public Animal {
public:
    void makeSound() { cout << "Meow" << endl; }
    void eat() { cout << "Cat is eating fish." << endl; }
};

在上面的代码中,我们重写了 Cat 类中的 eat 函数,它的输出是 “Cat is eating fish.”。而在 Dog 类中,我们没有重写 eat 函数,所以它沿用了父类 Animal 中的默认实现,输出是 “Animal is eating.”。

总结一下,纯虚函数必须由子类具体实现,而虚函数可以有默认的实现,但是也可以被子类重写。纯虚函数是定义接口的一种方式,而虚函数则提供了一种默认的实现。

9.3 什么叫抽象类?抽象类有何作用?抽象类的派生类是否一定要给出纯虚函数的实现?

带有纯虚函数的类是抽象类。其主要作用是通过它为一个类族建立一个公共的接口,使他们能够更有效地发挥多态特性。抽象类声明了一个类族派生类的共同接口,而接口的完整实现,即纯虚函数的函数体,要由派生类自己定义。抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以定义自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的实现,此时的派生类仍然是一个抽象类。

10.比较值传递和引用传递的相同点和不同点

值传递:是指当发生函数调用时,给形参分配内存空间,并用实参来初始化形参(直接将实参的值传递给形参)。这一过程是参数值的单向传递过程,一旦形参获得了值便与实参脱离关系,此后无论形参发生了怎样的改变,都不会影响到实参。

引用传递:将引用作为形参,在执行主调函数中的调用语句时,系统自动用实参来初始化形参。这样形参就成为实参的一个别名,对形参的任何操作也就直接作用于实参。

11.复制构造函数(Copy Constructor),复制构造函数何时被调用?

复制构造函数(Copy Constructor)是一种特殊的构造函数,用于创建一个新对象,该新对象是已经存在的另一个对象的精确副本(即使使用了动态分配的内存)。复制构造函数是通过引用参数传递的,其形参是本类的对象的引用 (const T &)。其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象
(1) 当用类的一个对象去初始化该类的另一个对象时;
(2) 如果函数的形参是类的对象,调用函数时进行形参和实参结合时;
(3) 如果函数的返回值是类的对象,函数执行完成返回调用者时。

class MyClass {
  public:
    int x;
    MyClass(int a) { x = a; } // 普通构造函数
    MyClass(const MyClass &obj) { // 复制构造函数
        x = obj.x;
    }
};

int main() {
  MyClass obj1(10); // 调用普通构造函数
  MyClass obj2 = obj1; // 调用复制构造函数
  cout << obj2.x;
  return 0;
}

12. 复制构造函数与赋值运算符(=)有何不同?

复制构造函数和赋值运算符都是用来创建一个对象的副本,但它们的作用和用法是不同的。

  • 复制构造函数是用来创建新对象并将其初始化为现有对象的副本。它是在对象被创建时调用的,通常通过拷贝构造函数定义为类的一个方法。复制构造函数通常采用引用参数,以便接受要复制的对象的地址,并返回新对象的副本。当需要创建新对象时,可以调用已有对象的复制构造函数。
  • 赋值运算符则用于将已有对象的值复制到另一个已有对象中。它是在对象已经存在时调用的,并且是通过重载赋值运算符(=)来定义的。赋值运算符通常采用引用参数,以便接受要复制的对象的地址,并返回对当前对象的引用。当需要将一个已有对象的值复制到另一个已有对象中时,可以使用赋值运算符。

总之,复制构造函数是用于创建新对象的,而赋值运算符是用于复制一个已有对象的值到另一个已有对象中

13.什么叫作用域?有哪几种类型的作用域?

作用域讨论的是标识符的有效范围,作用域是一个标识符在程序正文中有效的区域。

13.1.C++的作用域分为:

在C++中,常见的作用域包括全局作用域、局部作用域、命名空间作用域和类作用域。下面是每种作用域的简要说明和示例:

  1. 全局作用域(Global Scope): 全局作用域中声明的变量、函数等在整个程序中都可见。

    #include <iostream>
    
    int globalVar = 10; // 全局变量
    
    void globalFunction() { // 全局函数
        std::cout << "This is a global function." << std::endl;
    }
    
    int main() {
        std::cout << globalVar << std::endl; // 访问全局变量
        globalFunction(); // 调用全局函数
        return 0;
    }
    
  2. 局部作用域(Local Scope): 局部作用域中声明的变量、函数等只在其所在的代码块内可见。

    #include <iostream>
    
    int main() {
        int localVar = 20; // 局部变量
        std::cout << localVar << std::endl; // 在局部作用域内访问局部变量
        {
            int innerVar = 30; // 内部局部变量
            std::cout << innerVar << std::endl; // 在内部作用域内访问内部局部变量
        }
        // std::cout << innerVar << std::endl; // 无法在外部作用域访问内部局部变量
        return 0;
    }
    
  3. 命名空间作用域(Namespace Scope): 命名空间作用域中声明的变量、函数等在整个命名空间内可见。

    #include <iostream>
    
    namespace MyNamespace {
        int namespaceVar = 40; // 命名空间变量
    
        void namespaceFunction() { // 命名空间函数
            std::cout << "This is a namespace function." << std::endl;
        }
    }
    
    int main() {
        std::cout << MyNamespace::namespaceVar << std::endl; // 访问命名空间变量
        MyNamespace::namespaceFunction(); // 调用命名空间函数
        return 0;
    }
    
  4. 类作用域(Class Scope): 类作用域中声明的成员变量、成员函数等在类的实例中可见。

    #include <iostream>
    
    class MyClass {
    public:
        int memberVar = 50; // 类成员变量
    
        void memberFunction() { // 类成员函数
            std::cout << "This is a member function." << std::endl;
        }
    };
    
    int main() {
        MyClass myObj;
        std::cout << myObj.memberVar << std::endl; // 访问类成员变量
        myObj.memberFunction(); // 调用类成员函数
        return 0;
    }
    

这些作用域可以在程序中灵活组合使用,以实现对变量和函数的合理封装和访问控制。

13.2.作用域辨析有哪些?

在C++中,作用域辨析(Resolution)是指编译器根据标识符的名称解析出正确的变量、函数等实体的过程。作用域辨析包括以下几种情况:

  1. 名称冲突解决: 当在不同的作用域中出现相同名称的标识符时,编译器需要确定使用哪个标识符。这种情况下,编译器会根据作用域查找规则和命名空间限定符等信息来解析标识符。

  2. 成员访问: 当在类或命名空间中访问成员时,编译器需要确定使用哪个作用域中的成员。这包括成员变量、成员函数等的访问。

  3. 函数重载: 当调用一个函数时,如果有多个同名函数(函数名相同,但参数列表不同)可供选择,编译器需要根据参数列表的匹配情况来选择正确的函数。

  4. 模板辨析: 在使用模板时,编译器需要根据模板参数的类型推断和匹配来实例化正确的模板函数或类。

  5. 作用域限定符: 使用作用域限定符(如 ::)可以明确指定要使用的作用域,以避免歧义或者访问特定作用域中的成员。

这些都是作用域辨析的常见情况,编译器会根据标识符的上下文和声明信息来解析和确定正确的实体。

14. 什么叫做可见性?可见性的一般规则是什么?

在编程中,可见性(Visibility)指的是一个标识符(如变量、函数、类等)在代码中的可见程度或可访问性。可见性规则是指确定在特定位置上哪些标识符可以被正确访问的规则。

在C++中,可见性规则可以总结如下:

  1. 作用域规则: 标识符的可见性受限于其声明所在的作用域。在全局作用域中声明的标识符可以在整个程序中访问,而在局部作用域中声明的标识符只能在其所在的代码块内访问。

  2. 命名空间规则: 命名空间提供了一种将全局变量、函数等组织在一起的机制。在同一命名空间中声明的标识符可以相互访问,但在不同命名空间中声明的标识符需要使用命名空间限定符(::)来访问。

  3. 类成员规则: 在类中声明的成员变量和成员函数可以被该类的实例访问。类的成员函数可以访问该类的所有成员,包括私有成员。另外,派生类(子类)可以访问其基类(父类)的保护成员。

  4. 友元关系规则: 友元函数或友元类可以访问其所在类的私有成员。这允许某些函数或类在不是成员的情况下访问类的私有信息,但是需要谨慎使用以避免破坏封装性。

这些规则确保了代码中的标识符可以被正确访问和使用,同时提供了一定程度的封装性和安全性。

15. 什么叫静态数据成员?他有何特点?

类的静态数据成员:是类的数据成员的一种特例,采用 static 关键字来声明。对于类的普通数据成员,每一个类的对象都拥有一个拷贝,就是说每一个对象的同名数据成员可以分别存储不同的值,这也是保证对象拥有区别于其他对象的特征的需要,但是静态数据成员,每个类只要一个拷贝,由所有该类的对象共同维护和使用,这个共同维护,使用也就实现了同一类的不同对象之间的数据共享。

class MyClass {
  public:
    static int count;
    MyClass() { count++; }
};

int MyClass::count = 0;// 静态成员变量需要在类外部进行初始化


int main() {
  MyClass obj1;
  MyClass obj2;
  MyClass obj3;
  cout << "Number of objects created: " << MyClass::count << endl;
  return 0;
}

在上面的例子中,count是一个静态数据成员,它被定义为公共的,因此可以从类外部访问。在 MyClass 的构造函数中,每次创建 MyClass 对象时,count 的值都会加 1。在主函数中,我们创建了三个 MyClass 对象,并输出了创建对象的总数。最终,程序会输出——Number of objects created: 3

16. 什么叫静态函数成员?他有何特点?

使用 static 关键字声明的函数成员是静态的,静态函数成员属于整个类,被同一个类的所有对象共同维护,为这些所有对象共享。
静态成员函数具有以下两个方面的好处:

  • 由于静态成员函数只能直接访问同一个类的静态数据成员,可以保证不会对该类的其余数据成员造成负面影响;
  • 同一个类只维护一个静态函数成员的拷贝,节约了系统的开销,提高程序的运行效率。
class MyClass {
private:
  static int count;
public:
  static void incrementCount() {
    ++count;
  }
  
  void printCount() {
    std::cout << "Current count is: " << count << std::endl;
  }
};

int MyClass::count = 0; // 静态成员变量需要在类外部进行初始化

int main() {
  MyClass obj1, obj2;
  obj1.incrementCount(); // 调用静态函数成员
  obj2.incrementCount();
  obj1.printCount();
  obj2.printCount();
  return 0;
}

在这个程序中,我们定义了一个名为 MyClass 的类,其中包含一个静态成员变量 count 和一个静态函数成员 incrementCount ,这个静态函数成员可以用来增加 count 的值,而另一个成员函数 printCount 则用来输出当前 count 的值。在程序中,我们创建了两个 MyClass 类的对象 obj1 和 obj2,并分别对其调用 incrementCount 函数,最后输出当前的 count 值。从输出结果可以看出:
当我们调用 incrementCount 函数时,它并不需要依赖于具体的对象,所以它可以成为静态函数成员。

17. 什么叫友元函数?什么叫友元类?

友元函数:是使用关键字 friend 关键字声明的函数,它可以访问相应类的保护成员和私有成员。

class MyClass {
  private:
    int privateData;

  public:
    MyClass(int data): privateData(data) {}

    friend void printPrivateData(MyClass obj); // 声明为友元函数
};

void printPrivateData(MyClass obj) {
    std::cout << "私有数据为:" << obj.privateData << std::endl;
}

int main() {
    MyClass obj(10);
    printPrivateData(obj);
    return 0;
}

在上面的示例程序中,printPrivateData 函数被声明为 MyClass 的友元函数,可以访问 MyClass 类的私有数据成员 privateData。

友元类:是使用 friend 关键字声明的类,它的所有成员函数都是相应类的友元函数。

class MyClass1 { 
  private:
    int privateData;
  
  public: 
    MyClass1(int data): privateData(data) {}

    // 声明 MyClass2 为 MyClass1 的友元类
    friend class MyClass2;
};

class MyClass2 {
  public:
    void printPrivateData(MyClass1 obj) {
        std::cout << "私有数据为:" << obj.privateData << std::endl;
    }
};

int main() {
    MyClass1 obj1(10);
    MyClass2 obj2;
    obj2.printPrivateData(obj1);
    return 0;
}

在上面的示例程序中,MyClass2 类被声明为 MyClass1 类的友元类,可以访问 MyClass1 类的私有数据成员 privateData。在 printPrivateData 函数内,通过 obj.privateData 访问了 MyClass1 的私有数据成员。

友元不可继承,没有传递性。

18. 在函数内部定义的普通局部变量和静态局部变量在功能上有何不同?计算机底层对这两类变量做了怎样的不同处理导致了这种差异?

  1. 存储位置:普通局部变量存储在栈内存中,而静态局部变量存储在静态数据区(或者称为全局数据区)中。
  2. 生命周期:普通局部变量的生命周期与其所在的函数或代码块相关。当函数或代码块执行完成后,普通局部变量会被自动销毁。而静态局部变量的生命周期是整个程序运行期间,在第一次使用时初始化,在程序结束时销毁。
  3. 初始化和保持状态:普通局部变量每次进入函数或代码块时都会重新初始化,每次离开函数或代码块时都会被销毁。而静态局部变量在第一次使用时进行初始化,并且在函数或代码块退出后仍然保持其值,下次再进入时保持上一次的值。
  4. 可见性和作用域:普通局部变量仅在定义它的函数或代码块内可见,其他函数或代码块无法直接访问。而静态局部变量也具有局部作用域,只能在定义它的函数或代码块内部访问,但与普通局部变量不同的是,静态局部变量在整个函数或代码块范围内都可见,即使超出了其定义的地方,仍然可以访问。

19.引用和指针有何区别?何时只能使用指针而不能使用引用?

引用是一个别名,不能为 NULL 值,不能被重新分配;
指针是一个存放地址的变量。
当需要对变量重新赋以另外的地址或赋值为 NULL 时只能使用指针。
引用是一个变量的别名,本身不单独分配自己的内存空间,而指针有自己的内存空间。

20.比较类的 3 种继承方式之间的差别?

公有继承,使得基类 public(公有)和 protected(保护)成员的访问属性在派生类中不变, 而基类 private(私有)成员不可访问。
私有继承,使得基类 public(公有)和 protected(保护)成员都以 private(私有)成员身份出 现在派生类中,而基类 private(私有)成员不可访问。
保护继承中,基类 public(公有)和 protected(保护)成员都以 protected(保护)成员身份出 现在派生类中,而基类 private(私有)成员不可访问。
在这里插入图片描述

21.什么叫虚基类?有何作用?

当某类的部分或全部直接基类是从另一个基类派生而来,这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,派生类的对象的这些同名成员在内存中同时拥有多个拷贝,我们可以使用作用域分辨符来唯一标识并分别访问它们。
我们也可以将直接基类的共同基类设置为虚基类,这时从不同的路径继承过来的该类成员在内存中只拥有一个拷 贝,这样就解决了同名成员的唯一标识问题。

虚基类的声明是在派生类的定义过程,其语法格式为: class 派生类名:virtual 继承方式基类名上述语句声明基类为派生类的虚基类,在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。

声明了虚基类之后,虚基类的成员在进一步派生过程中,和派生类一起维护一个内存数据拷贝。 虚基类就是为了解决多继承产生的二义性问题

#include <iostream>
using namespace std;

class Animal {
public:
    Animal() {
        cout << "Animal 构造函数" << endl;
    }
    virtual ~Animal() {
        cout << "Animal 析构函数" << endl;
    }
};

class Mammal : virtual public Animal {
public:
    Mammal() {
        cout << "Mammal 构造函数" << endl;
    }
    virtual ~Mammal() {
        cout << "Mammal 析构函数" << endl;
    }
};

class Bird : virtual public Animal {
public:
    Bird() {
        cout << "Bird 构造函数" << endl;
    }
    virtual ~Bird() {
        cout << "Bird 析构函数" << endl;
    }
};

class Bat : public Mammal, public Bird {
public:
    Bat() {
        cout << "Bat 构造函数" << endl;
    }
    ~Bat() {
        cout << "Bat 析构函数" << endl;
    }
};

int main() {
    Bat bat;
    return 0;
}
输出:
Animal 构造函数
Mammal 构造函数
Bird 构造函数
Bat 构造函数
Bat 析构函数
Bird 析构函数
Mammal 析构函数
Animal 析构函数

在上述例子中,Animal 是一个虚基类,它被 Mammal 和 Bird 类同时继承。而 Bat 类则继承了 Mammal 和 Bird 类。
如果不进行虚继承,Mammal 和 Bird 类同时继承Animal将会导致Animal类中函数的二义性,因为Mammal和Bird都有相同的Animal中的函数,而Bat又继承了Mammal和Bird

通过使用虚基类,我们避免了 Animal 对象在 Bat 类中重复生成的问题。输出结果显示,在构造 Bat 对象时,Animal 的构造函数只被调用了一次。同样地,在析构 Bat 对象时,Animal 的析构函数也只被调用了一次。

22.组合与继承有什么共同点和差异?通过组合生成的类与被组合的类之间的逻辑关系是什 么?继承呢?

组合(Composition)和继承(Inheritance)是两种对象之间关系的实现方式。
组合和继承它们都使得已有对象成为新对象的一部分,从而达到代码复用的目的。组合和继承其实反映了两种不同的对象关系。

  • 组合反映的是 “有一个”(has-s)的关系,如果类 B 中存在一个类 A 的内嵌对象,表示的是每一个 B 类型的对象都“有一个”A类型的对象,A 类型的对象与 B 类型的对象是部分整体的关系。
  • 继承反映的是“是一个”(is-a)的关系,在“是一个”关系中,如果类 A 是类 B 的公有基类,那么这表示每一个 B
    类型的对象都“是一个”A 类型的对象,B 类型的对象与 A 类型的对象是特殊与一般的关系。

23.基类与派生类的对象,指针或引用之间,哪些情况下可以隐含转换,哪些情况下可以显 示转换?在涉及多重继承或虚继承的情况下,在转换时会面临哪些新问题?

派生类指针可以隐含转换为基类指针,而基类指针要想转换为派生类指针,则转换一定要显示地进行。因为从特殊的指针转换到一般的指针时安全的,因此允许隐含转换;
一般的指针转换到特殊的指针不安全的,因此只能显示地转换。基类对象一般无法被显式转换为派生类对象。

  • 多重继承情况下:执行基类指针到派生类指针的显式转换时,有时需要将指针所存储的地址值进行调整后才能得到新指针的值。
  • 虚继承情况下:如果 A 类型是 B 类型的虚拟基类,虽然 B 类型的指针可以隐含转换为 A 类型,但是 A 类型指针却无法通过
    static_case 隐含转换为 B 类型的指针。
#include <iostream>

class Animal {
public:
    virtual void makeSound() {
        std::cout << "Animal makes a sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Dog barks" << std::endl;
    }

    void wagTail() const {
        std::cout << "Dog wags tail" << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Cat meows" << std::endl;
    }

    void purr() {
        std::cout << "Cat purrs" << std::endl;
    }
};

int main() {
    Animal animal;
    Dog dog;
    Cat cat;

    // 隐式转换:派生类对象可以隐式转换为基类对象
    Animal* animalDog = &dog;
    Animal* animalCat = &cat;

    animalDog->makeSound(); // 输出 "Dog barks"
    animalCat->makeSound(); // 输出 "Cat meows"

    // 显式转换:基类对象指针需要显式转换为派生类对象指针
    Animal* animalPtr = &dog;
    Dog* dogPtr = static_cast<Dog*>(animalPtr);
    dogPtr->wagTail(); // 输出 "Dog wags tail"

    return 0;
}

在这个示例中,animalPtrDog 和 animalPtrCat 是基类指针,分别指向 Dog 和 Cat 的对象,这样就实现了派生类指针向基类指针的隐式转换。
在这个例子中,我们首先将 dog 对象的地址存储在 animalPtr 中,然后使用 static_cast 将 animalPtr 转换为 dogPtr。这样就可以调用 Dog 类中特有的方法 wagTail()。

24.什么叫做多态性?在 C++种是如何实现多态的?

多态是指同样的消息被不同类型的对象接收时导致完全不同的行为,是对类的特定成员函数的再抽象。C++支持重载多态,强制多态,包含多态和参数多态。

在基类中声明相应的函数为 virtual 型,然后在派生类中实现该函数,这样就可以通过基类指针调用派生类对象的函数,实现了运行时动态绑定,即多态的功能。

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void makeSound() { //声明virtual类型函数,在子类中实现(重载)
        cout << "Animal makes sound" << endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        cout << "Dog barks" << endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() override {
        cout << "Cat meows" << endl;
    }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->makeSound();  // 输出:Dog barks
    animal2->makeSound();  // 输出:Cat meows

    delete animal1;
    delete animal2;
    
    return 0;
}

在上述例子中,Animal 是基类,Dog 和 Cat 是派生类。它们都重写了 makeSound() 方法。

通过使用基类指针 Animal* ,我们可以在运行时动态绑定不同的派生类对象,并调用它们各自的 makeSound()方法。这就展示了多态的概念,相同的函数调用具有不同的行为,根据实际的派生类类型来决定具体执行哪段代码。

25.在 C++中,能否声明虚构造函数?为什么?能否声明虚析构函数?有何用途?

在 C++中,不能声明虚构造函数,多态是不同的对象对同一消息有不同的行为特性,虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的, 因此虚构造函数是没有意义的;

可以声明虚析构函数,析构函数的功能是在该类对象消亡之前进行一些必要的清理工作, 如果一个类的析构函数是虚函数,那么,由它派生而来的所有子类的析构函数也是虚函数。 析构函数设置为虚函数之后,在使用指针引用时可以动态联编,实现运行时的多态,保证使用基类的指针就能够调用适当的析构函数针对不同的对象进行清理工作。

#include<iostream>
using namespace std;

class A {
public:
    A() {
        cout<<"A()" << endl;
    }
    virtual void print()const {
        cout << "Im A" << endl;
    }
    //此处不加上virtual变成虚析构函数,那么B的析构函数不会调用
    virtual ~A() { 
        cout<<"~A()" << endl;
    }
};

class B:public A {
public:
    B() {
        cout << "B()" << endl;
    }
    void print()const {
        cout << "Im B" << endl;
    }
    ~B() {
        cout << "~B()" << endl;
    }
};

void main() {
    A* pa = new B();
    pa->print();
    delete pa;
}

~A()不加上virtual变成虚析构函数,那么B的析构函数不会调用

26.什么叫做流?流的提取和插入是指什么?I/O 流在 C++中起着怎样的作用?

流是一种抽象,它负责在数据的生产者和数据的消费者之间建立联系,并管理数据的流动,一般意义下的读操作在流数据抽象中被称为(从流中)提取,写操作被称为(向流中)插入。
操作系统是将键盘、屏幕、打印机和通信端口作为扩充文件来处理的,I/O 流类就是用来与这些扩充文件进行交互,实现数据的输入与输出。

27.什么叫做异常?什么叫做异常处理?

当一个函数在执行的过程中出现了一些不平常的情况,或运行结果无法定义的情况,使得操作不得不被中断时,我们说出现了异常。异常通常是用 throw 关键字产生的一个对象, 用来表明出现了一些意外的情况。我们在设计程序时,就要充分考虑到各种意外情况,并给 与恰当的处理。这就是我们所说的异常处理。

28.C++的异常处理机制有何优点?

C++的异常处理机制使得异常的引发和处理不必在同一函数中,这样底层的函数可以着 重解决具体问题,而不必过多地考虑对异常的处理。上层调用者可以在适当的位置设计对不同类型异常的处理。

29. 比较函数重载和虚函数在概念和使用方式方面有什么区别?

函数重载和虚函数是面向对象编程中常用的特性,它们有以下的区别:

  • 函数重载:
    函数重载是指在一个类中声明多个同名函数,但它们的参数列表不同。在调用这些函数时编译器会根据实参的类型和数量自动选择匹配的函数。因此,函数重载可以提供多种不同的操作方式。
  • 虚函数:
    虚函数是在基类中声明的一个虚函数,子类可通过重写该函数实现自己版本的函数。在调用虚函数时,根据当前对象的真实类型来确定运行哪个版本的函数。虚函数实现了多态的特性,可以实现类的动态绑定。使用虚函数可以在进行多态编程时解除对具体类型的依赖,使代码具备更加灵活的扩展性。

总的来说:

函数重载可以用于普通函数(非成员的函数)和类的成员函数,而虚函数只能用于类的成员函数。
函数重载可以用于构造函数,而虚函数不能用于构造函数。

如果对成员函数进行重载,则重载的函数与被重载的函数应当都是同一类中的成员函数,不能分属于两个不同继承层次的类。函数重载是横向的重载。
虚函数是对同一类族中的基类和派生类的同名函数的处理,即允许在派生类中对基类的成员函数重新定义。虚函数的作用是处理纵向的同名函数。

重载的函数必须具有相同的函数名,但函数的参数个数和参数类型二者中至少有一样不同,否则在编译时无法区分它们。而虚函数则要求再同一类族中的所有虚函数不仅函数名相同,而且要求函数类型、函数的参数个数和参数类型都全部相同,否则就不是重定义了。也就不是虚函数了。

函数重载是在程序编译阶段确定操作的对象的,属静态关联。虚函数是在程序运行阶段确定操作的对象的,属动态关联。

30.比较 class 和 struct 结构体的异同

不同之处:
默认成员访问权限不同,struct 默认是 public,而 class 默认是 private。
默认继承方式不同,struct 默认是 public,而 class 默认是 private。
在定义模板参数时可以用 class 但不能用 struct。

相同之处在于:
都可以包含成员函数,都可以实现继承,都可以实现多态。

31.浅拷贝和深拷贝的区别?

在C++中,浅拷贝和深拷贝是用来描述对象复制的两种不同方式。

浅拷贝(Shallow Copy):浅拷贝仅仅复制对象的成员变量的值,而不复制对象指向的堆内存。如果对象内部有指针指向堆内存,浅拷贝只会复制指针的值,两个对象的指针指向同一块堆内存。这可能会导致潜在的问题,因为当一个对象销毁时,会释放内存,但另一个对象可能仍然在使用该内存,导致野指针错误或者内存泄漏。

深拷贝(Deep Copy):深拷贝会复制对象的所有成员变量,包括指针指向的堆内存,创建一个新的对象,并将所有的数据复制过去。这样,两个对象完全独立,对其中一个对象的修改不会影响另一个对象。深拷贝通常需要自定义拷贝构造函数和析构函数来确保指针指向的内存得到正确的复制和释放。

32.const 和 static 作用 const 作用,静态变量和静态函数:

const:
定义 const 常量,具有不可变性,保护被修饰的东西。防止意外修改,增强程序的 健壮性。
便于类型检查,使编译器对处理内容有更多的了解。
可以避免意义模糊的数字出现,也便于进行参数的修改。同宏定义一样,可以做到不变则以,一变都变。
const 定义的常量在程序运行过程中只有一份拷贝,而宏定义却又若干份拷贝。

static:
作为类的静态数据成员:被类的所有对象共同拥有,在内存中只有一份拷贝。在类内声明,在类外初始化。
在全局变量前加上 static 关键字,该变量被定义为一个全局变量,
特点:
a) 变量在全局数据区分配内存
b) 如果不显示初始化,则隐式的初始化为 0
c) 作用域仅限于本文件内

局部变量:前加 static 关键字,该变量被定义为静态局部变量
特点:
a) 在全局数据区分配内存
b) 如果不显示初始化,则隐式的初始化为 0
c) 始终驻留在全局数据区,直到程序结束。作用域仅限于函数体内,当函数执行完后作用域随之消失。

静态函数:前加static关键字,该函数被定义为静态函数
特点:
a) 静态函数只能在本源文件中使用
b) 在文件作用域下声明的 inline 函数默认是 static 的

32.析构函数带 virtual 和不带 virtual 的区别

如果基类要被派生,则析构函数一定要带 virtual。否则,通过基类指针指向派生类对象后,删除该指针时并不会释放派生类对象的空间。也即,不会调用派生类的析构函数。

不带 virtual 的几种情况:

作为非公有基类
不作为接口使用的基类
可以保证这个类不被 public 继承
如果他的所有派生类的 virtual 函数都是什么都不做
如果不需要基类指针指向派生类对象

33.重写,重载和隐藏的区别?

  • 重写(覆盖):父类和子类存在相同的函数,包括返回值,参数列表均相同,父类的该函数必须含有 virtual 关键字。
  • 重载:在一个类中的同名函数,但参数列表不同,函数的返回值相同。
  • 隐藏:派生类的函数屏蔽了与其同名的父类函数——是指在派生类中定义了与基类同名的成员变量或成员函数,从而隐藏了基类的同名成员。这样一来,在派生类对象中,访问同名成员时,只能访问到派生类中的成员,而无法访问基类中的成员。

34.C++中的四个默认的函数(构造函数、拷贝函数、析构函数、赋值函数)

  • 构造函数:先基类后派生类
  • 拷贝函数:对象被创建时调用
  • 调用拷贝构造函数的情况:
    • 一个对象以值传递的方式传入函数体
    • 一个对象以值传递的方式从函数返回
    • 一个对象需要通过另外一个对象初始化
  • 析构函数:每个类只有一个析构函数
  • 赋值函数:是一种特殊的成员函数,用于将一个对象的值复制给另一个对象。在 C++ 中,赋值函数的函数名为 operator=,它是一个重载运算符。赋值函数常常被自动生成,但也可以显式地定义和重载。赋值函数通常与复制构造函数一起使用,用于初始化对象和将一个对象赋值给另一个对象。

35.不可以被重载的运算符——五个

类属关系运算符:. .
成员指针运算符:*
作用域运算符: ::
sizeof 运算符:sizeof()
三目运算符:a == b ? 1 :2

36.三种字符数组初始化的方法

char str[5] = {“hello”};
char str[6] = {‘a’,‘b’,‘c’,‘d’,‘e’,’\0’};
char str[] = “abcde”;

37.哪些操作符必须重载为成员函数

必须重载:
= -> () [] new delete
一元操作符:+、-、、/、%、&、|、~、!、++、- -(一元操作符:只有一个操作数)
二元操作符:+、-、
、/、%、^、&、|、<<、>>、==、!=、>=、<=、>、<、&&、||

38.什么是“else 摇摆问题”,举例说明

C++编译器总是把 else 同与它最近的 if 联系起来,因此如果没有正确放置花括号对就会出现逻辑错误。

if (condition1) {
    // Code block 1
} else {
    if (condition2) {
        // Code block 2
    } else {
        // Code block 3
    }
}

用下列代码解决:使用else if

if (condition1) {
    // Code block 1
} else if (condition2) {
    // Code block 2
} else {
    // Code block 3
}

39.函数模板和函数重载的区别与联系

函数重载:C++允许使用同一个函数名,不同的参数个数和参数类型来定义多个函数。 重载函数的参数个数,类型,顺序至少有一个不同。返回值可以相同,也可以不同。

函数模板:是对一类同构函数的抽象定义,并不是具体的函数,函数模板的定义被编译时不会产生任何可执行的代码。

#include <iostream>

// 定义一个函数模板,交换a与b的值
template <typename T>
void swapValues(T &a, T &b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;
    cout << "Before swapping: x = " << x << ", y = " << y << endl;

    // 调用函数模板来交换两个整数
    swapValues(x, y);

    cout << "After swapping: x = " << x << ", y = " << y << endl;

    double a = 3.14, b = 6.28;
    cout << "Before swapping: a = " << a << ", b = " << b << endl;

    // 调用函数模板来交换两个浮点数
    swapValues(a, b);

    cout << "After swapping: a = " << a << ", b = " << b << endl;

    return 0;
}

在这个例子中,add 函数是一个函数模板,它的类型参数为 T,即可以处理各种类型的输入参数。

联系:针对不同类型的数据,但实现相同功能的函数重载可以通过函数模板来替代。
函数模板:通常用于处理各种相同类型的输入参数
函数重载:则通常用于处理不同类型或数量的输入参数。

40.怎样区别虚函数和纯虚函数?两者都有什么作用

纯虚函数是在基类中声明的虚函数,在基类中没有定义。在基类中实现纯虚函数的方法是在函数原型后加“=0。虚函数可以实现也可以不实现。

定义纯虚函数是为了实现一个接口,起到了一个规范的作用。规范继承这个类的程序员必须实现这个接口。

声明了纯虚函数的类为抽象类,不能实例化对象。 定义虚函数是为了允许用基类的指针来调用子类的这个函数。

抽象类的作用:将有关的操作作为一个接口组织在一个继承层次结构中,由他来为派生类提供一个公共的根。派生类将具体实现在其基类中作为接口的操作。

41.面向对象程序“接口与实现方法分离”,有什么作用(好处)

接口与实现分离技术可以只把接口暴露给用户,具体的实现细节隐藏起来,当需要改动代码时,只要在实现部分修改后编译,用户无需再编译自己的项目。

  • 实现代码重用:通过接口与实现的分离,我们可以更加灵活地对系统进行设计和编码,使得不同的实现可以共享相同的接口,并且可以更加容易地被复用和扩展。
  • 稳定接口定义:接口定义是相对稳定而且不经常变化的,将接口与实现分离可以避免接口的频繁变化,从而减少了对系统的影响,保持了代码的稳定性和可维护性。
  • 实现了抽象化:通过接口定义,我们可以将系统中的实现细节隐藏起来,从而实现了抽象化,使得我们可以更关注于系统的接口和功能,而不需要关注具体的实现细节。
  • 简化编译依赖:将接口与实现分离可以消除编译时的依赖关系,减少编译时间,提高编译效率,从而让产品更快地上市。

42.列出所有与字符串处理有关的头文件

  • string.h:是C语言中字符串操作函数的头文件
  • cstring:是 c++对 C 语言中的 strcpy 之类的函数申明,包含 cstring 之后,就可以在程序中使用 C 语言风格的
    strcpy之类的函数。
  • string:是 c++语言中 string 类模板的申明
  • CString:是 MFC 中定义的字符串类,MFC 中很多类及函数都是以 CString 为参数的
  • cstdlib:也是C语言标准库中的头文件,包含了一些与字符串处理相关的函数,如字符串转换为数值、随机数生成等。
  • cctype:定义了一系列用于字符分类的函数,例如判断字符是否是数字、字母等。

43.C++中显示类型转换

  1. 静态转换(static_cast):用于执行编译时的类型转换,可以在相关类型之间进行转换,如基本数据类型、指针或引用。静态转换提供了一种较为安全的类型转换方式,但需要程序员确保转换的安全性。

    double d = 3.14;
    int i = static_cast<int>(d);
    
  2. 动态转换(dynamic_cast):用于在继承关系中进行安全的向下转型(派生类向基类)或运行时检查,只能用于具有多态性的类层次结构中。如果无法进行安全的转换,则返回空指针或引发 std::bad_cast 异常。

创建一个基类指向派生类的指针,改指针向下转型成了Derived

#include <iostream>

class Base {
public:
    virtual ~Base() {}  // 基类必须有虚析构函数
};

class Derived : public Base {};

int main() {
    Base* basePtr = new Derived;

    // 向下转型,进行类型检查
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

    if (derivedPtr) {
        std::cout << "Downcast successful." << std::endl;
    }
    else {
        std::cout << "Downcast failed." << std::endl;
    }

    delete basePtr;
    return 0;
}

  1. 常量转换(const_cast):用于在指针或引用中添加或去除 const 限定符,主要用于解除 const 对象的只读属性或者将非 const 对象转换为 const 对象。

通过nonConstPtr, 我们修改了指向常量的指针constPtr的数值

#include <iostream>

int main() {
    const int value = 10;
    const int* constPtr = &value; // 声明一个指向常量的指针
    int* nonConstPtr = const_cast<int*>(constPtr); // 将指针的const属性去除

    // 修改非const指针指向的值
    *nonConstPtr = 20;

    std::cout << "Modified value: " << *constPtr << std::endl; // 打印修改后的值

    return 0;
}

  1. 重新解释转换(reinterpret_cast)
    非常非常危险!!!

重新解释转换(reinterpret_cast)是C++中最危险的类型转换之一,它可以将任何指针类型转换为另一种指针类型,也可以将整数类型转换为指针类型,或者反之。

重新解释转换的语法如下:

reinterpret_cast<new_type>(expression)

其中,new_type是要转换到的类型,expression是要转换的指针、整数或引用。

由于重新解释转换绕过了C++的类型系统,因此它可能导致未定义的行为或安全问题,应尽量避免在正常的程序中使用。

下面是一些示例用法:

  1. 将整数转换为指针:
int intValue = 42;
int* intPtr = reinterpret_cast<int*>(&intValue);
  1. 将指针类型转换为另一种指针类型:
int* intPtr = new int(10);
char* charPtr = reinterpret_cast<char*>(intPtr);
  1. 将指针转换为整数:
int* intPtr = new int(10);
uintptr_t intValue = reinterpret_cast<uintptr_t>(intPtr);

重新解释转换的使用应该非常小心,并且只在必要时才使用,通常情况下最好避免使用重新解释转换。

44.delete 和 delete [] 的区别?

在C++中,用于释放动态分配的内存的两个关键操作符是 deletedelete[]

  1. delete:用于释放使用 new 分配的单个对象的内存。它与 new 配合使用,用于销毁动态分配的单个对象,并释放相应的内存。如果使用 new 来分配一个单个对象,就应该使用 delete 来释放它。

    int* ptr = new int; // 动态分配一个单个整数对象
    delete ptr; // 释放分配的内存
    
  2. delete[]:用于释放使用 new[] 分配的数组对象的内存。它与 new[] 配合使用,用于销毁动态分配的数组,并释放相应的内存。如果使用 new[] 来分配一个数组,就应该使用 delete[] 来释放它。

    int* arr = new int[10]; // 动态分配一个包含10个整数的数组
    delete[] arr; // 释放分配的内存
    

使用 deletedelete[] 来释放动态分配的内存是很重要的,否则会导致内存泄漏问题。务必确保在不再需要动态分配的内存时正确地使用 deletedelete[] 进行释放。

45.存储类别说明符可划分为两类

  1. 静态存储类别(static,extern):静态存储类在程序的整个生命周期内都存在,可以在全局范围或局部范围内使用
    static:用于定义静态变量,静态变量在程序的整个执行过程中都存在,并且只在定义它的作用域内可见。
    extern:用于声明一个全局变量,它并不会分配存储空间,而是引用另一个已经定义的全局变量。
  2. 自动存储类别(auto, register):自动存储类是默认的存储类,不需要显式地指定。
    auto:默认的存储类,用于定义自动变量。
    register:用于请求将变量存储在寄存器中,但具体是否存储在寄存器中取决于编译器。

这样的变量在程序执行进入到定义它们的程序块时创建,在程序块激活时存在,在程序块退出时销毁。

46.什么是运算符重载,它如何增强 C++的扩展性?

运算符重载是对已有的运算符赋予多重含义,使得同一个运算符作用于不同类型的数据时导致不同的行为。
运算符重载的本质是函数重载,可以改变现有运算符的操作方式,以适用于类类型,为类的用户提供了一个直接的接口,使得用户程序所用的语言是面向问题的,而不是面向机器的,增强了 C++的扩展性。

47.STL 是什么?组成部分和区别。

STL (Standard Template Library)是 c++提供的标准模板库。
STL 的主要组成部分为:容器、迭代器、算法、函数对象和适配器。其中算法处于核心地位,迭代器如同算法和容器之间的桥梁,算法通过迭代器从容器中获取元素,然后将获取的元素传递给特定的函数对象的操作,最后将处理后的结果存储到容器中

48.什么是逻辑错误?什么是语法错误?请举例说明。

逻辑错误:由于程序设计人员设计的算法有错或编写的程序有错,此时程序一般能够正常运行,但是通知给系统的指令与解题的原意不相同,即出现了逻辑上的混乱。

int a[5] = { 1 , 2 , 3 , 4 , 5} ; 
for( int i = 0 ; i <= 0 ; i ++ ) cout << a[i] << endl ; //发生数组越界

语法错误:违背了 C++语言的规定,不能生成可执行文件,主要是语句的结构或拼写中 存在的错误。

for( int i = 0 , i < 10 , i ++) { } //应该用“;

49.编写语句说明枚举类型是如何定义和使用的。

枚举类型的声明形式如下 enum 枚举类型名 { 变量值列表 } ;

enum weekday{ sun , mon , tue , wed , thu , fri , sta } ; 
enum weekday a , b , c = tue ; //也可以省略 enum

枚举元素具有默认值,他们依次是:0 , 1 , 2 , … , 例子中:sun 的值为 0 , mon 的值 为 1,tue 的值为 2,… ,sta 的值为 6

  • 枚举元素按常量处理,不能进行赋值。
  • 整数值也不能赋给枚举变量,如确实需要可进行强制类型转换。
  • 枚举变量可以进行关系运算。

50.ADT(Abstract Data Type )是什么?简述你对“数据抽象”和“信息隐藏”的认识。

C++内部的数据类型包括基本类型和复合类型(数组、字符串、指针和结构),有表示范围,只是用可以接受的方式表示实际概念。确定了一个类型就确定了计算机存储给类型所 需要的容量,确定了其表示范围,也确定了对该类型可以进行的操作。

抽象数据类型(Abstract Data Type),简称 ADT,是指一个数学模型以及定义在该模型 上的一组操作。 通常以以下格式定义: ADT 抽象数据类型名{ 数据对象:<数据对象的定义> 数据关系:<数据关系的定义> 基本操作:<基本操作的定义> }

数据抽象:对具体事物描述的一个概括。通过数据抽象可以将数据类型的定义和它的实 现分开,使得只研究和使用它的结构而不用考虑它的实现细节成为可能。C++中的类就是一 种数据抽象,类是具有相同属性和服务的一组对象的集合。

信息隐藏:C++中的封装就是信息隐藏的一种,即尽可能的隐藏对象的内部细节,对外形成一个边界,只保留有限的对外接口使之与外部反生关系。

51.简述你对“面向对象”和“面向过程”编程思想的理解和认识。

  • “面向过程”(POP-Procedure-Oriented Programming)是一种以事件为中心的编程思想,就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。过程化编程强调功能,以过程模块为中心,分层逐步展开设计。通常采用结构化程序设计,基本思路为:自顶向下、逐步求精。

    面向过程编程将程序视为一系列按照特定顺序执行的步骤或过程。在这种编程思想中,重点放在算法,通过依次执行步骤来完成

  • “面向对象”(OOP-Object-Oriented Programming)的编程思想就是把你需要解决的问题中的所有具体的东西,都看成一个个具有属性和行为的对象,然后把所有具有相同性质的对象抽象成类,那些与问题无关的对象则忽略。对象化编程强调分离抽象层次,以便让程序员分工,关心不同抽象层次中的细节,而不用去关心不同抽象层次的联系,数据安全而隐蔽,不同抽象层次的职责分明。

    主要特点:
    封装:将数据和操作封装到一个对象中,通过公共接口访问对象的状态和行为。
    继承:通过继承机制,从已有类派生出新类,并可以重用已有类的属性和方法。
    多态:同一操作可以使用不同对象进行调用,可以根据对象的类型执行不同的操作,提供了灵活性和扩展性。

    优点:
    可重用性:面向对象编程强调模块化和封装,在设计良好的类和对象之间可以进行复用,减少代码重复。
    扩展性:通过继承和多态的特性,可以方便地添加、修改和扩展代码,使得应用程序更易于维护和扩展。
    抽象性:可以将复杂的问题分解为一系列相互协作的对象,使得代码更易理解、更接近实际问题。

52.写出递归函数定义以及相应的优缺点

递归函数是直接或间接地(通过另一个函数)调用自己。

优点:
结构清晰,可读性强,使程序易于理解和调试。

缺点:
效率较低。递归是有时间和空间消耗的。另外,递归有可能很多计算都是重复的,从而给性能带来很大的负面影响。因为递归的本质是把一个问题分解成连个或者多个小问题。如果多个小问题存在相互重叠的部分,那么就存在重复的计算。

可能导致调用栈溢出。每一次函数调用在内存栈中分配空间,而每个进程的栈的容量是有限的。当递归调用的层级太多时,就会超出栈的容量,从而导致栈溢出。

53.写出包含输入输出的标准库

  1. <iostream>:这个头文件包含了用于标准输入输出的类和对象,比如 cincoutcin 用于标准输入,通常用于从键盘读取用户输入的数据;cout 用于标准输出,通常用于向屏幕输出数据。

  2. <iomanip>:这个头文件包含了用于设置输入输出格式的函数和控制符,比如 setwsetprecision 等。可以用于控制输出的精度、宽度等。

  3. <cstdio>:这个头文件是 C 标准库中的一部分,提供了类似于 C 语言中的输入输出函数,比如 printf()scanf()。虽然它不是C++标准库的一部分,但在C++中也可以使用。

  4. <fstream>:这个头文件提供了用于文件输入输出的类和函数,比如 ifstreamofstreamfstream。它们允许你读取和写入文件。

  5. <ofstream>:输出文件

#include <iostream>
#include <fstream> // 包含文件流头文件
#include <string>

using namespace std;

int main() {
    ofstream outFile("output.txt"); // 创建一个输出文件流对象,并指定文件名
    if (!outFile) { // 检查文件是否成功打开
        cerr << "Error opening file!" << endl; // 输出错误信息
        return 1;
    }

    string text = "Hello, world!"; // 要写入的文本
    outFile << text << endl; // 将文本写入文件
    outFile.close(); // 关闭文件流

    cout << "Text written to file successfully!" << endl;

    return 0;
}

  1. <sstream>:这个头文件提供了用于字符串流的类和函数,比如 istringstreamostringstreamstringstream。它们允许你在内存中操作字符串,类似于文件流的操作。

  2. <iomanip>:此前提到过,这个头文件提供了用于设置输入输出格式的函数和控制符,比如 setwsetprecision 等。

54.数学函数

在C++中,常用的数学函数包括:

  1. abs():返回一个整数或浮点数的绝对值。
  2. sqrt():返回一个浮点数的平方根。
  3. pow():返回一个数的指定次幂。
  4. exp():返回自然对数的指数。
  5. log():返回一个数的自然对数。
  6. sin()cos()tan():分别返回一个数的正弦、余弦和正切值,参数为弧度。
  7. asin()acos()atan():分别返回一个数的反正弦、反余弦和反正切值,返回值以弧度表示。
  8. fabs():返回一个浮点数的绝对值。
  9. ceil()floor():分别返回大于或等于、小于或等于一个浮点数的最小整数值。
  10. round():返回浮点数的四舍五入整数值。

这些函数都在 <cmath> 头文件中声明

55.String函数

标准库 <string> 中提供了许多函数来处理字符串。以下是一些常用的字符串函数:

  1. length():返回字符串的长度(字符数)。
  2. size():返回字符串的长度(字符数),与 length() 函数作用相同。
  3. empty():检查字符串是否为空,返回 truefalse
  4. clear():清空字符串,使其变为空字符串。
  5. append(str):在字符串末尾添加另一个字符串 str
  6. insert(pos, str):在指定位置 pos 插入字符串 str
  7. erase(pos, len):从指定位置 pos 开始删除长度为 len 的子串。
  8. replace(pos, len, str):将指定位置 pos 开始的长度为 len 的子串替换为字符串 str
  9. substr(pos, len):返回从指定位置 pos 开始,长度为 len 的子串。
  10. find(str):在字符串中查找子串 str,返回子串首次出现的位置,如果不存在返回 string::npos
  11. rfind(str):从字符串末尾开始查找子串 str,返回子串最后一次出现的位置,如果不存在返回 string::npos
  12. getline(inputStream, str, delimiter):从输入流 inputStream 中读取一行文本,存储到字符串 str 中,直到遇到分隔符 delimiter
  13. compare(str):与另一个字符串 str 进行比较,返回比较结果。

这些是 C++ 中标准库 <string> 中常用的一些字符串函数。通过使用这些函数,可以方便地对字符串进行各种操作。

56.C语言字符串函数

strcpystrncpystrcatstrncatstrcmp 等函数是C标准库中用于操作字符串的一部分函数,它们可以在C++中使用。这些函数的作用如下:

  1. strcpy(char *dest, const char *src):将源字符串 src 复制到目标字符串 dest 中,直到遇到空字符 ‘\0’ 为止。
  2. strncpy(char *dest, const char *src, size_t n):将源字符串 src 的前 n 个字符复制到目标字符串 dest 中,如果 src 的长度小于 n,则剩余部分用空字符 ‘\0’ 填充。
  3. strcat(char *dest, const char *src):将源字符串 src 追加到目标字符串 dest 的末尾,并在目标字符串的末尾添加空字符 ‘\0’。
  4. strncat(char *dest, const char *src, size_t n):将源字符串 src 的前 n 个字符追加到目标字符串 dest 的末尾,如果 src 的长度小于 n,则剩余部分用空字符 ‘\0’ 填充,并在目标字符串的末尾添加空字符 ‘\0’。
  5. strcmp(const char *str1, const char *str2):比较两个字符串 str1str2,如果它们相等则返回0;如果 str1 小于 str2,则返回小于0的值;如果 str1 大于 str2,则返回大于0的值。

这些函数都是在处理C风格字符串时非常有用的,但在C++中更推荐使用标准库 <string> 中提供的字符串类 std::string,因为它提供了更多的功能,并且更安全、更易于使用。

要将 std::string 转换为 char* 数组,可以使用 c_str() 成员函数。它返回一个指向以空字符结尾的字符数组的指针,该字符数组包含了 std::string 对象中的内容。例如:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, world!";
    const char* charArray = str.c_str();
    
    std::cout << "String as char array: " << charArray << std::endl;
    
    return 0;
}

56.1要将 char* 数组string 对像的相互转化

以下是示例代码:

#include <iostream>
#include <string>

using namespace std;

int main() {
    // string 转换为 char*
    string str = "Hello, world!";
    const char* charArray = str.c_str();
    cout << "String as char array: " << charArray << endl;

    // char* 转换为 string
    const char* charArray2 = "Hello, C++!";
    string str2(charArray2);
    cout << "String: " << str2 << endl;

    return 0;
}

这样就完成了 stringchar* 之间的相互转换

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值