1. C++三大特性
-
面向对象编程(Object-Oriented Programming,OOP):C++支持面向对象编程范式,允许程序员以类、对象、继承、封装、多态等方式组织代码,提高代码的可维护性、可重用性和可扩展性。
-
泛型编程(Generic Programming):C++支持泛型编程,即以不依赖于特定数据类型的方式编写代码,使得代码更加灵活、通用。泛型编程的代表性特征是模板(template)。
-
高效性(Efficiency):C++被设计为一门高效的语言,它的许多特性都是为了提高程序的执行效率。例如,C++支持直接操作内存,可以进行低级别的指针操作和位运算等操作,同时也提供了高级的抽象层次,如迭代器(iterator)和算法(algorithm),帮助程序员在不牺牲效率的情况下提高代码的可读性和可维护性。
-
数据抽象:接口和实现分离 继承:基类派生类 多态:动态绑定
2. C++构造函数可以是虚函数吗?
C++ 构造函数不能是虚函数。在 C++ 中,虚函数是通过在类中定义一个虚函数表来实现的,这个表存储了指向每个虚函数的指针。由于在构造函数被调用之前,对象还没有被创建,因此虚函数表还不存在,所以在构造函数中无法使用虚函数。
此外,由于构造函数的特殊用途,它们没有返回类型,因此它们不能被声明为虚函数。因此,如果在构造函数中使用 virtual
关键字,编译器将发出错误消息。
总之,C++ 构造函数不能是虚函数。
3. C++ 析构函数可以是虚函数吗?为什么要将析构函数设置为虚函数
C++析构函数可以是虚函数。在C++中,如果一个类中至少有一个虚函数,那么它的析构函数应该被声明为虚函数。这是因为在使用多态性时,如果基类指针指向派生类对象,当使用delete操作符释放基类指针时,如果基类的析构函数不是虚函数,则只会调用基类的析构函数而不会调用派生类的析构函数。这可能会导致内存泄漏和未定义行为。
如果将析构函数声明为虚函数,当使用delete操作符释放基类指针时,会按照对象的实际类型(而不是指针类型)调用相应的析构函数,从而确保正确地释放对象。
例如:
class Base {
public:
virtual ~Base() {}
};
class Derived : public Base {
public:
~Derived() {}
};
int main() {
Base* b = new Derived();
delete b; // 调用Derived的析构函数
return 0;
}
在这个示例中,由于Base的析构函数被声明为虚函数,当使用delete操作符释放Derived对象时,会自动调用Derived的析构函数。
4. 实现懒汉式单例和饿汉式单例
单例模式是一种设计模式,它确保一个类只有一个实例,并提供全局访问点。在 C++ 中,可以使用懒汉式和饿汉式来实现单例。
懒汉式单例
懒汉式单例是指在第一次使用时才创建单例对象。以下是使用懒汉式实现单例的示例代码:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() {}; // 构造函数私有化,防止外部创建实例
Singleton(const Singleton&); // 防止拷贝构造函数
Singleton& operator=(const Singleton&); // 防止赋值操作
};
在上述代码中,使用了一个静态变量 instance
来保存单例对象的实例。在 getInstance()
函数中,当程序第一次调用该函数时,会创建一个 Singleton
对象的实例,并将其赋值给 instance
变量。之后,每次调用 getInstance()
函数时,都会返回这个静态变量 instance
的值,从而实现单例的懒汉式。
注意到上述代码中,Singleton
的构造函数、拷贝构造函数和赋值操作都被声明为私有的。这是为了防止外部直接创建实例,从而保证单例的唯一性。
饿汉式单例
饿汉式单例是指在程序启动时就创建单例对象。以下是使用饿汉式实现单例的示例代码:
class Singleton {
public:
static Singleton& getInstance() {
return instance;
}
private:
Singleton() {}; // 构造函数私有化,防止外部创建实例
Singleton(const Singleton&); // 防止拷贝构造函数
Singleton& operator=(const Singleton&); // 防止赋值操作
static Singleton instance; // 静态变量,用于保存单例对象的实例
};
Singleton Singleton::instance;
在上述代码中,使用了一个静态变量 instance
来保存单例对象的实例。在程序启动时,会创建一个 Singleton
对象的实例,并将其赋值给 instance
变量。之后,每次调用 getInstance()
函数时,都会返回这个静态变量 instance
的值,从而实现单例的饿汉式。
同样地,Singleton
的构造函数、拷贝构造函数和赋值操作都被声明为私有的,以保证单例的唯一性。
5. C++类中数据成员的初始化顺序
-
静态成员变量的初始化:如果类有静态成员变量,则它们将在程序开始执行前进行初始化。
-
成员变量的初始化:成员变量的初始化顺序按照它们在类中声明的顺序进行初始化。例如,如果类声明了一个成员变量A和一个成员变量B,那么A将在B之前初始化。
-
构造函数的执行:构造函数是在所有成员变量都被初始化之后才执行的。在构造函数中可以对成员变量进行进一步的初始化操作。
需要注意的是,如果成员变量是基类或成员对象,则其构造函数也将在它们自己的初始化列表中按照上述规则进行初始化。这个初始化列表是在构造函数的函数体之前执行的。
总之,了解C++类中数据成员初始化的顺序对于正确设计和使用类非常重要,可以帮助我们避免一些常见的错误和问题
6. 实例化一个对象需要几个阶段
-
分配内存:首先,为对象分配内存,这个内存的大小是由对象的大小决定的。如果对象的类型是由类定义的,那么该类的大小将包括类的成员变量和函数。
-
调用构造函数:一旦分配了内存,就会调用构造函数来初始化该对象。构造函数是一种特殊的函数,它在对象创建时自动调用,并设置对象的初始状态。如果对象的类型没有构造函数,则对象将被初始化为默认状态。
-
返回指针或引用:最后,返回一个指向该对象的指针或引用,这个指针或引用可以用来访问该对象的成员变量和函数。
总之,实例化一个对象需要分配内存,调用构造函数和返回指针或引用。这些步骤通常是由编译器隐式执行的,因此在代码中看不到它们的实现。
7. C++偏特化
在C++中,模板是一种强大的机制,它可以使代码更加通用和灵活。偏特化是模板的一个扩展,它允许我们对模板参数进行更细粒度的控制,从而实现更精细的模板行为。
偏特化是指对模板中的某些参数进行特化,以满足特定的需求。C++中有两种偏特化形式:类模板偏特化和函数模板偏特化。
类模板偏特化是指对模板类的某些参数进行特化,以满足特定的需求。例如,我们可以定义一个通用的模板类,但是特定类型的参数需要特殊的处理,此时我们可以使用类模板偏特化。例如:
template <typename T>
class MyClass {
public:
void doSomething() { /* ... */ }
};
// 偏特化
template <>
class MyClass<int> {
public:
void doSomething() { /* special handling for int */ }
};
在上面的例子中,我们定义了一个通用的模板类MyClass
,但是对于int
类型的参数,我们需要做特殊的处理。因此,我们使用了偏特化,将MyClass<int>
特化为一个新的类,重写了doSomething()
函数。
函数模板偏特化是指对模板函数的某些参数进行特化,以满足特定的需求。例如:
template <typename T>
void myFunc(T arg) {
// ...
}
// 偏特化
template <>
void myFunc(int arg) {
// special handling for int
}
在上面的例子中,我们定义了一个通用的模板函数myFunc
,但是对于int
类型的参数,我们需要做特殊的处理。因此,我们使用了偏特化,将myFunc<int>
特化为一个新的函数,重写了函数体。
总之,偏特化是C++中一种非常强大的特性,它允许我们对模板进行更精细的控制,以满足特定的需求。在实际编程中,我们可以根据实际情况选择使用偏特化,使代码更加通用、灵活和高效。
8. 举例说明一下C++偏特化
C++的偏特化是指对泛型类型进行特化,使其适用于一定范围的类型,而不是全部的类型。在C++中,可以通过以下方式来定义偏特化。
假设有一个泛型类MyClass
,其中包含一个泛型类型参数T
:
template<typename T>
class MyClass {
public:
void print() {
std::cout << "MyClass<T>" << std::endl;
}
};
现在我们希望对MyClass
进行偏特化,使其只适用于指针类型。可以这样定义偏特化:
template<typename T>
class MyClass<T*> {
public:
void print() {
std::cout << "MyClass<T*>" << std::endl;
}
};
这样定义后,当我们使用MyClass
时,如果传入的类型是指针类型,就会使用偏特化后的版本。例如:
MyClass<int> a; // 使用原始版本
a.print(); // 输出 "MyClass<T>"
MyClass<int*> b; // 使用偏特化版本
b.print(); // 输出 "MyClass<T*>"
通过偏特化,我们可以针对不同的类型进行不同的操作,从而更加灵活地使用泛型编程。
9. C++中重载,重写,覆盖的区别
C++中的重载(Overloading)、重写(Override)和覆盖(Overriding)是面向对象编程中的基本概念,它们的区别如下:
- 重载(Overloading)
重载是指在同一个作用域内,通过改变函数参数列表的方式,定义多个同名函数的过程。重载函数可以有不同的参数列表和返回值类型。编译器根据函数调用时所提供的实参类型和数量来决定调用哪个重载函数。
int add(int x, int y) {
return x + y;
}
double add(double x, double y) {
return x + y;
}
这两个函数都是名为add的重载函数,分别接受不同类型的参数,可以用于对int和double类型的数值进行加法运算。
2. 重写(Override)
重写是指派生类重新定义基类中已有的虚函数的过程。重写函数的名称、参数列表和返回类型必须与基类的虚函数完全相同,而且它们的访问修饰符也必须相同或更宽松。
例如:
class Base {
public:
virtual void print() {
cout << "I am a base class." << endl;
}
};
class Derived : public Base {
public:
void print() {
cout << "I am a derived class." << endl;
}
};
在上面的代码中,我们定义了一个基类Base和一个派生类Derived。在基类中,我们定义了一个名为print的虚函数,而在派生类中,我们重新定义了这个函数。当我们通过基类的指针或引用调用print函数时,实际上会调用派生类中的print函数,从而实现了多态性。
3. 覆盖(Shadowing)是指在派生类中定义了与基类同名的成员变量或成员函数,从而“遮盖”了基类中的同名成员。当我们在派生类中使用这个成员时,实际上是访问派生类中定义的成员,而不是基类中的同名成员。例如
class Base {
public:
int num = 10;
void print() {
cout << "I am a base class." << endl;
}
};
class Derived : public Base {
public:
int num = 20;
void print() {
cout << "I am a derived class." << endl;
}
};
int main() {
Derived d;
cout << d.num << endl; // 输出20
d.print(); // 输出“I am a derived class.”
return 0;
}
在上面的代码中,我们定义了一个基类Base和一个派生类Derived。在基类中,我们定义了一个名为num的成员变量和一个名为print的函数,而在派生类中,我们定义了一个名为num的成员变量和一个名为print的函数。当我们创建一个Derived
10. C++指针和引用的区别
C++指针和引用都是用来访问内存地址的机制,但是它们有以下不同点:
-
定义方式:指针是一个变量,存储一个内存地址,而引用则是一个别名,是被引用对象的一个别名。
-
可空性:指针可以是NULL(空),表示不指向任何对象;引用必须始终引用一个对象,不能为NULL。
-
操作方式:指针可以被重新赋值指向其他对象,也可以进行指针运算,如指针加减、指针比较等;引用一旦被绑定到一个对象,就不能再引用其他对象,也没有指针运算。
-
访问方式:通过指针访问对象需要使用操作符;通过引用访问对象不需要使用操作符。
-
传递方式:指针作为函数参数时,需要传递指针变量的地址;引用作为函数参数时,直接传递引用变量即可。
-
数据类型:指针有自己的数据类型,可以指向不同类型的对象;引用没有自己的数据类型,必须与被引用对象类型匹配。
总的来说,引用更为安全和简洁,因为它不需要使用指针运算符和地址运算符,也不需要进行空指针检查,而指针则更为灵活,可以指向不同类型的对象,并且可以进行指针运算。
11. 智能指针有哪些,以及实现原理
智能指针是一种 C++ 的语言特性,它可以自动管理动态分配的内存,避免内存泄漏和悬挂指针等错误。C++ 中常用的智能指针有以下三种:
-
unique_ptr
:独占式智能指针,一个对象只能有一个unique_ptr
指向它。当unique_ptr
被销毁时,它指向的对象也会被销毁。unique_ptr
实现的原理是通过使用 RAII(资源获取即初始化)技术,利用一个指向动态分配内存的原始指针和一个删除器(deleter)对象来管理内存的生命周期。 -
shared_ptr
:共享式智能指针,多个对象可以共享一个shared_ptr
指向它们。当最后一个shared_ptr
被销毁时,它指向的对象才会被销毁。shared_ptr
实现的原理是通过在堆上分配一个引用计数器(reference counter)对象,用来记录有多少个shared_ptr
指向该对象,并在引用计数器为 0 时删除对象。 -
weak_ptr
:弱引用智能指针,它可以指向一个shared_ptr
管理的对象,但不会增加引用计数。weak_ptr
实现的原理是通过在堆上分配一个计数器对象和一个指向被管理对象的指针,用来记录有多少个weak_ptr
指向该对象,并在引用计数器为 0 时同时删除对象和计数器。
智能指针的实现原理是基于 RAII 技术和引用计数器实现的。RAII 技术是指在对象的构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来管理资源的生命周期。引用计数器是指在堆上分配一个计数器对象,用来记录有多少个智能指针指向该对象,并在计数器为 0 时释放对象。
例如,shared_ptr
的实现原理如下:
-
shared_ptr
内部包含一个指向被管理对象的指针和一个指向引用计数器对象的指针。 -
当一个
shared_ptr
拷贝构造或拷贝赋值时,引用计数器会增加 1。 -
当一个
shared_ptr
被销毁时,引用计数器会减少 1,如果引用计数器变为 0,则同时删除被管理对象和引用计数器对象。 -
当一个
shared_ptr
对象拷贝构造或拷贝赋值给另一个shared_ptr
对象时,它们共享同一个引用计数器,因此可以共享同一个被管理对象。
总的来说,智能指针是 C++ 中一种非常有用的语言特性,它可以自动管理动态分配的内存,避免内存泄漏和悬挂指针等错误,其实现原理是基于 RAII 技术和引用计数器实现的。
智能指针是 C++ 中非常有用的语言特性,它可以自动管理动态分配的内存,避免内存泄漏和悬挂指针等错误。以下是几个智能指针的使用场景:
- 动态分配对象:在 C++ 中,我们通常使用
new
运算符来动态分配对象,但是容易忘记释放内存。使用unique_ptr
可以自动管理动态分配的内存,避免内存泄漏和悬挂指针等错误。例如:
std::unique_ptr<int> p(new int(42));
- 防止内存泄漏:在使用动态分配内存的代码中,如果发生异常或者提前返回,很容易导致内存泄漏。使用智能指针可以避免这种情况发生。例如:
void foo()
{
std::unique_ptr<int> p(new int(42));
// do some work
if (error_occurs) {
return; // p will be destroyed automatically
}
// do some more work
}
- 共享对象:在多个对象之间共享同一个对象时,使用
shared_ptr
可以避免重复释放内存的问题。例如:
class A {
public:
void set_ptr(std::shared_ptr<A> p) {
ptr_ = p;
}
private:
std::shared_ptr<A> ptr_;
};
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(new A());
p1->set_ptr(p2);
p2->set_ptr(p1); // p1 and p2 share the same object
- 避免多次释放内存:在 C++ 中,一个对象可能被多个指针所指,如果手动释放内存时容易出现多次释放的问题。使用
weak_ptr
可以避免这种情况发生。例如:
class A {
public:
void set_ptr(std::weak_ptr<A> p) {
ptr_ = p;
}
private:
std::weak_ptr<A> ptr_;
};
std::shared_ptr<A> p1(new A());
std::shared_ptr<A> p2(new A());
p1->set_ptr(p2);
p2->set_ptr(p1); // p1 and p2 share the same object, but neither owns it
总的来说,智能指针是一种非常有用的语言特性,它可以自动管理动态分配的内存,避免内存泄漏和悬挂指针等错误。在动态分配对象、防止内存泄漏、共享对象和避免多次释放内存等场景中,使用智能指针可以提高代码的可靠性和安全性。
12. 如何避免循环依赖
1. 使用指针代替变量声明
class A
{
public:
B *b;
};
class B
{
public:
A *a;
};
这样在编译时并不会报错,因为指针类型就是四个字节,在编译时编译器知道A、B类所占内存空间的大小,故在编译时不会报错。
2. 既然A、B两个类相互包含说明A、B两个类的耦合度比较高,则可以将A、B声明为一个类,然后使用派生,将A、B声明为该类的子类,修改所需的变量即可。
13. 虚函数的原理、多态的底层实现
虚函数的原理和多态的底层实现是 C++ 实现面向对象编程的重要机制之一。
在 C++ 中,虚函数的原理是通过虚函数表(Virtual Table,简称 VTable)来实现的。每个包含虚函数的类都有一个对应的虚函数表,该表存储了该类中所有虚函数的指针。当一个对象被创建时,它会包含一个指向对应虚函数表的指针(通常称为虚指针),该指针指向该对象所属类的虚函数表。当调用一个虚函数时,实际调用的是该对象所属类的虚函数表中对应虚函数的指针所指向的函数。
多态的底层实现是基于虚函数的原理实现的。多态的概念是指,一个对象可以根据其实际类型来表现出多种不同的行为。在 C++ 中,多态的实现方式主要有两种:静态多态和动态多态。
静态多态是指通过函数重载和模板实现的多态,它在编译期间就可以确定调用的函数或模板实例。静态多态的实现方式是通过函数重载和模板的特化和泛化来实现的。
动态多态是指通过虚函数和指针或引用实现的多态。动态多态的实现方式是通过虚函数表和虚指针来实现的。在动态多态中,通过父类指针或引用指向子类对象,可以实现对子类的多态访问。例如:
class Animal {
public:
virtual void speak() {
cout << "Animal is speaking." << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "Dog is barking." << endl;
}
};
int main() {
Animal* animal = new Dog();
animal->speak(); // 输出 "Dog is barking."
delete animal;
return 0;
}
在上面的例子中,我们定义了一个 Animal
类和一个 Dog
类,其中 Animal
类有一个虚函数 speak
,而 Dog
类重写了该虚函数。在 main
函数中,我们创建了一个 Dog
对象,并将其赋值给一个 Animal
指针,然后调用该指针的虚函数 speak
,实际调用的是 Dog
类中重写的虚函数,输出 "Dog is barking."。
总的来说,虚函数的原理和多态的底层实现是 C++ 实现面向对象编程的重要机制之一,可以让我们更方便地编写可重用、可扩展和易维护的代码。
14.虚函数和纯虚函数的区别
在 C++ 中,虚函数和纯虚函数都是用于实现多态的重要机制。它们之间的区别如下:
-
虚函数(Virtual Function):是在基类中被声明为虚函数的成员函数,可以被派生类重新定义,实现多态。虚函数可以有默认的实现,如果在派生类中没有重新定义该函数,则会调用基类中的实现。虚函数可以通过在函数声明前加上
virtual
关键字来声明,例如:class Animal { public: virtual void move() { cout << "Animal is moving." << endl; } }; class Dog : public Animal { public: void move() override { cout << "Dog is running." << endl; } };
在上面的例子中,我们定义了一个
Animal
类和一个Dog
类。在Animal
类中,我们声明了一个名为move
的虚函数,并给出了其默认的实现。在Dog
类中,我们重新定义了move
函数,实现了狗的奔跑动作。在程序中,我们可以通过Animal
类的指针或引用来调用move
函数,实现多态的效果。 -
纯虚函数(Pure Virtual Function):是在基类中声明但没有定义的虚函数,必须在派生类中重新定义并实现,不能在基类中给出默认的实现。纯虚函数可以通过在函数声明前加上
virtual
关键字,并在函数声明后加上= 0
来声明,例如:class Shape { public: virtual double area() = 0; }; class Rectangle : public Shape { public: double area() override { return width * height; } private: double width, height; };
在上面的例子中,我们定义了一个
Shape
类和一个Rectangle
类。在Shape
类中,我们声明了一个纯虚函数area
,并没有给出默认的实现。在Rectangle
类中,我们重新定义了area
函数,实现了矩形的面积计算。需要注意的是,由于Shape
类中的area
函数是纯虚函数,因此不能创建Shape
类的对象,只能通过派生类来使用。
总之,虚函数和纯虚函数都是用于实现多态的重要机制。虚函数可以被派生类重新定义,也可以在基类中给出默认的实现,而纯虚函数必须在派生类中重新定义并实现,不能在基类中给出默认的实现。纯虚函数常常用作抽象类的接口,而虚函数常常用于实现多态。
15.C++的内存对齐
C++ 的内存对齐是为了优化内存访问速度和提高性能的机制。内存对齐的规则是,结构体或类中的每个成员变量都会被对齐到其自然边界上的内存地址处,自然边界是指该成员变量所占用的内存大小,通常是该类型的大小或其倍数。
内存对齐的原因是,硬件对于内存的读写操作是以固定的块大小为单位进行的,如果数据没有对齐到块的边界上,就需要进行额外的移位操作,会影响访问速度和性能。例如,一个 int 类型的变量通常占用 4 个字节,如果该 int 变量的地址不是 4 的倍数,那么访问它就需要额外的移位操作,会降低访问速度和性能。
C++ 中可以通过 alignas
关键字来指定内存对齐的大小,例如:
struct alignas(8) MyStruct {
char a;
int b;
char c;
};
在上面的例子中,我们使用 alignas(8)
来指定 MyStruct
类型的内存对齐大小为 8 字节。这意味着,MyStruct
中的每个成员变量都会被对齐到 8 字节的边界上。
需要注意的是,过度地进行内存对齐也会导致内存浪费,降低内存利用率。因此,需要根据实际情况进行内存对齐的优化,权衡内存利用率和访问速度和性能。
16. 定义函数指针,指针函数
指针函数:带指针的函数,即本质是一个函数。函数返回值类型是某一类型的指针。
类型标识符 * 函数名 (参数表) int *func(x,y) int* func(x,y)
eg: float *fun()
float *p;
p = func();
函数指针:指向函数(首地址)的指针变量,即本质是一个指针变量。
函数指针说的是一个指针,但这个指针指向的函数,不是普通的基本数据类型或者对象。
指向函数的指针包含了函数的地址,可以通过他来调用函数。
类型说明 (*函数名)(参数)
eg: int (*func)(int a, int b) 声明函数指针
最简单的辨别方式就是看函数名前的指针 * 号 有没有被括号 ()包含,如果被包含就是函数指针,反之则是指针函数。
17. C和C++中的struct的区别
在C语言和C++语言中,struct的基本定义方式是相同的,都是用于定义自定义的数据类型,可以包含多个不同类型的成员变量和成员函数。但在使用上,C语言和C++语言中的struct存在一些不同之处,主要包括:
1.在C中,struct定义的数据类型不能直接使用成员函数,需要通过函数指针或结构体变量来调用。而在C++中,struct可以包含成员函数,可以像类一样使用成员函数。
2.在C中,struct定义的变量需要使用关键字struct来声明,例如struct Student s。而在C++中,可以直接使用Student s来声明变量,不需要使用关键字struct。
3.在C++中,struct可以继承其他的struct或者class,支持面向对象的特性,可以定义构造函数,析构函数,虚函数等。
4.在C++中,struct的默认访问权限是public,而class的默认访问权限是private。这意味着,在struct中定义的成员变量和成员函数可以被外部直接访问,而在类中定义的成员变量和成员函数默认情况下是不能被外部直接访问的。
18. C和C++中的static区别
1.在C语言中static可以用于全局变量和函数,表示他们的作用域限定在当前文件中,不会被其他文件访问到。在函数内部使用static修饰局部变量,则该变量的生命周期域程序运行的整个过程相同,只会被初始化一次。
2.在C++中,static关键字可以用于类的成员变量和成员函数,表示它们属于类本身,而不是对象,在内存中只有个一个实例,静态成员变量必须在类定义外部进行初始化,静态成员函数只能访问静态成员变量或函数,不能访问非静态成员变量或函数。
C语言中主要用于限定作用域,而c++中的static主要作用于定义类的静态成员。
19. 抽象函数能不能实例化
抽象函数(也称为纯虚函数)是C++中的一种特殊的虚函数,它没有函数体,只有声明,用来规范派生类中必须实现的接口。在类中包含纯虚函数的类被称为抽象类,不能直接实例化,只能作为基类来派生具体的子类。
因为抽象类中包含纯虚函数,所以不能直接实例化。如果尝试实例化一个抽象类,编译器会报错,提示不能实例化抽象类。例如:
class A {
public:
virtual void func() = 0; // 纯虚函数
};
int main() {
A a; // 编译错误,不能实例化抽象类
return 0;
}
上述代码中,类A是一个抽象类,包含一个纯虚函数func(),不能直接实例化。在主函数中尝试实例化一个A类型的对象a时,编译器会报错。
需要注意的是,如果派生类没有实现基类中的所有纯虚函数,那么它仍然被认为是抽象类,不能直接实例化。只有派生类实现了基类中的所有纯虚函数,才能被实例化。
20. 有什么方法保证每个头文件只被使用一次
可以使用头文件保护宏来确保一个头文件只被使用一次。头文件保护宏的作用是在头文件中定义一个宏,然后再头文件中的代码之前和之后分别加上宏的判断条件,以确保头文件只被编译一次。
例如:
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
// 头文件中的代码
#endif /*HEADER_FILE_NAME_H*/
21. 结构体字节对齐的原则和好处
结构体字节对齐是指编译器在为结构体分配内存空间时,按照一定的规则进行填充,使得结构体提中的每个成员都位于一个自然边界上,以提高访问结构体成员的效率。
在进行字节对齐时,编译器会根据一下原则进行填充:
1. 结构体的首地址必须是其成员中占用内存最大的类型的倍数。
2. 结构体中每个成员的地址都必须是他自身大小的倍数。
3. 结构体的总大小必须是其最宽基本类型成员大小的倍数。
例如,对于下面的结构体:
struct Person {
char name[10];
int age;
float height;
};
假设char类型占用1个字节,int类型4个字节,float类型4个字节,那么根据字节对齐的原则,编译器会在成员之间增加填充字节,使得结构体的各个成员的地址都能够满足对齐要求,从而提高访问结构体成员的效率。
结构体字节对齐的好处在于:
1. 提高访问结构体成员的效率,因为成员所在的地址都是对齐的,可以直接访问。
2. 减少内存碎片,避免因为内存对齐引起的额外内存浪费。
3. 保证结构体在不同的平台上具有相同的内存布局,从而增加代码的可移植性。
#pragma pack 指令是用来指定结构体的字节对齐方式的编译指令,他可以控制编译器如何为结构体分配内存空间。
#pragma pack(n)
其中 n 表示字节对齐的大小,通常是 1、2、4、8等值。当n为1时,表示取消对齐;当n为其他非负整数时,表示按照n的值进行对齐。
#pragma pack(1)
struct Person {
char name[10];
int age;
float height;
};
#pragma pack()
在上面的代码中,#pragma pack(1)
指令表示取消对齐,即按照字节对齐值为 1 来分配内存。这样,结构体中的各个成员都是按照一个字节对齐的,不会有额外的填充字节。在结构体定义结束后,使用 #pragma pack()
恢复默认的对齐方式。
需要注意的是,使用 #pragma pack
指令可能会影响程序的性能和可移植性,因此一般情况下应该尽量避免使用。在必要的情况下,可以根据具体的需求选择合适的字节对齐方式,并仔细测试程序的性能和可移植性
22. malloc 和 new的区别 delete p 和 delete [] p 的区别
1. malloc是C语言中的函数,而new是C++中的运算符
2. malloc只能分配内存,而new不仅可以分配内存,还可以调用对象的构造函数进行初始化。
3. malloc返回是 void *类型的指针,需要进行强制类型转换后才能使用,而new返回的是指定类型的指针。
4. new操作符会自动计算要分配的内存空间大小,而malloc需要手动指定要分配的内存空间的大小。
对于释放内存,使用delete 和 delete [] 都可以释放通过new分配的内存,但是他们的使用方式有所不同。
1. 对于new分配的单个对象,使用delete来释放内存,
例如:
int *p = new int;
// 使用 p 指向的内存
delete p;
2. 对于new分配的数组,使用delete []来释放内存,
例如:
int *p = new int[10];
// 使用 p 指向的内存
delete [] p;
需要注意的是,不能使用delete来释放new [] 分配的数组,也不能使用delete [] 来释放new分配的单个对象,否则会导致未定义的行为。
总的来说,对于 C++ 程序,推荐使用 new
和 delete
来进行内存分配和释放,因为它们可以进行对象的构造和析构,使得代码更加清晰和安全。但是在需要与 C 代码进行交互时,也可以使用 malloc
和 free
来进行内存分配和释放。
23. malloc和mmap的底层实现?malloc分配的是什么?
malloc和mmap都是动态分配内存的函数,他们的底层实现方式有所不同。
malloc函数的底层实现通常是基于内存池的方式,即在程序启动时预先分配一定大小的内存池,然后再需要分配内存时,从内存池中分配一段合适大小的内存空间,并在内存池中标记该内存空间已被使用。
malloc分配的内存空间通常是在堆上分配的,堆是由操作系统动态分配的一段连续的虚拟内存空间,malloc实际上是通过调用操作系统提供的分配堆内存的系统调用来实现的。在分配内存时,malloc会根据需要分配的内存大小和堆的状态来决定从堆的那个位置分配内存,如果堆中没有足够的连续内存空间,怎会触发堆的扩展或者使用其他内存池进行分配。
mmap
函数的底层实现通常是基于虚拟内存的方式,即在程序启动时预先创建一些虚拟内存区域,然后在需要分配内存时,从这些虚拟内存区域中选择一个空闲的区域,并在该区域中映射一段物理内存空间。
mmap
分配的内存空间通常是在虚拟内存中分配的,虚拟内存是由操作系统管理的一种抽象的内存空间,它将物理内存和虚拟内存进行了映射,使得程序可以像访问物理内存一样访问虚拟内存。在分配内存时,mmap
会根据需要分配的内存大小和虚拟内存区域的状态来决定从哪个虚拟内存区域中分配内存,如果虚拟内存区域中没有足够的空闲内存空间,则会触发虚拟内存的扩展或者使用其他内存池进行分配。
malloc分配的是一段连续的内存空间,具体内容取决于程序需要存储的数据类型和数据结构。在分配内存时malloc不会对分配的内存进行初始化,因此分配的内存中可能包含有未初始化的数据。如果需要对分配的内存进行初始化,可以使用calloc函数或者在分配内存后手动进行初始化。
24. 如何禁止构造函数的使用
在C++中,构造函数是用来初始化对象的函数,通常情况下不能禁止使用构造函数。但是如果想要禁止有两种方式。
1. 将构造函数声明为private或者protected,将构造函数声明为private或者protected可以使得外部无法调用该构造函数来创建对象。
class MyClass {
private:
MyClass() {} // 私有构造函数
};
int main() {
// 编译错误,无法调用私有构造函数
MyClass obj;
return 0;
}
2. 使用delete关键字,使用delete关键字可以在编译期间禁止使用构造函数
class MyClass {
public:
MyClass() = delete; // 禁止使用构造函数
};
int main() {
// 编译错误,无法使用构造函数
MyClass obj;
return 0;
}
需要注意的是,禁止使用构造函数可能会导致程序无法正常运行,因为对象没有被正确地初始化。因此,在禁止使用构造函数之前,需要仔细考虑程序的设计和需求,并谨慎决定是否禁止使用构造函数
25. 如何禁止类实例化时候的动态分配方式
在 C++ 中,类的实例化通常是通过动态分配内存来实现的,即使用 new
运算符来分配对象的内存空间。如果想要禁止类实例化时使用动态分配的方式,可以采取以下两个方法:
- 将类的构造函数声明为 private 或者 protected:将类的构造函数声明为 private 或者 protected 可以使得外部无法直接创建对象,从而禁止使用动态分配内存的方式。例如:
class MyClass {
private:
MyClass() {} // 私有构造函数
public:
static MyClass createInstance() {
return MyClass();
}
};
int main() {
// 编译错误,无法使用 new 运算符动态分配对象
MyClass* obj = new MyClass();
// 使用静态方法创建对象
MyClass obj2 = MyClass::createInstance();
return 0;
}
在这个例子中,将类的构造函数声明为 private 可以禁止外部直接创建对象,但是可以使用静态方法 createInstance()
来创建对象。
- 重载类的
new
运算符:可以重载类的new
运算符,使得在使用new
运算符分配内存时会触发编译错误。例如:
class MyClass {
public:
void* operator new(size_t) = delete; // 禁止使用 new 运算符动态分配内存
};
int main() {
// 编译错误,无法使用 new 运算符动态分配对象
MyClass* obj = new MyClass();
return 0;
}
在这个例子中,将类的 new
运算符重载为 delete
可以禁止使用 new
运算符动态分配内存。
需要注意的是,禁止使用动态分配内存的方式可能会影响程序的灵活性和扩展性,因此需要仔细考虑程序的设计和需求,并谨慎决定是否禁止使用动态分配内存的方式。
26. 实现一个类成员函数,不允许修改类的成员
在C++中,可以使用const限定符来声明一个类成员函数,并在函数内部禁止修改类的成员变量。具体如下:
class MyClass {
private:
int m_value;
public:
int getValue() const {
// 在 const 成员函数中不能修改成员变量的值
return m_value;
}
};
在这个例子中,getValue()
函数被声明为 const
成员函数,表示这个函数不会修改类的成员变量。在函数内部,不能通过 this
指针来修改成员变量的值,否则会导致编译错误。
需要注意的是,如果类的成员变量是指针类型,那么在 const 成员函数中不能修改指针所指向的对象的值,但是可以修改指针本身的值。例如:
class MyClass {
private:
int* m_ptr;
public:
int* getValue() const {
// 在 const 成员函数中不能修改指针所指向的对象的值
return m_ptr;
}
};
int main() {
MyClass obj;
const MyClass& constObj = obj;
int* ptr = constObj.getValue();
// 编译错误,不能将 ptr 指向的对象的值修改
*ptr = 10;
// 可以修改指针本身的值
ptr = nullptr;
return 0;
}
在这个例子中,getValue()
函数返回一个指向成员变量 m_ptr
的指针,但是在 const 成员函数中不能修改指针所指向的对象的值。在 main()
函数中,尝试修改指针所指向的对象的值会导致编译错误,但是可以修改指针本身的值。
总之,使用 const
成员函数可以禁止类的成员变量被修改,从而增强程序的安全性和稳定性。
27. C++中const的使用方法
在 C++ 中,const
是一个关键字,用于指定变量或者函数参数、函数返回值不可修改。const
可以应用于以下几个方面:
- 常量变量:使用
const
关键字可以声明一个常量变量,即变量的值不能被修改。例如:
const int MAX_SIZE = 100;
int arr[MAX_SIZE]; // 使用常量变量声明数组的大小
- 指针常量:使用
const
关键字可以将指针声明为常量,即指针指向的值不能被修改。例如:
int value = 42;
const int* ptr = &value; // 指针指向的值不能被修改
- 常量指针:使用
const
关键字可以将指针的指向声明为常量,即指针的指向不能被修改。例如:
int value = 42;
int* const ptr = &value; // 指针的指向不能被修改
- 常量成员函数:使用
const
关键字可以声明一个成员函数为常量成员函数,即在函数内部不能修改类的成员变量。例如:
class MyClass {
private:
int m_value;
public:
int getValue() const { // 声明为常量成员函数
// 在 const 成员函数中不能修改成员变量的值
return m_value;
}
};
- 常量函数参数:使用
const
关键字可以声明一个函数参数为常量,即函数内部不能修改函数参数的值。例如:
void printValue(const int value) { // 声明为常量函数参数
// 在函数内部不能修改 value 的值
std::cout << value << std::endl;
}
需要注意的是,const
关键字可以用于多个方面,但是它的含义和作用不同。在使用 const
时,需要根据具体的场景和需求来选择合适的使用方法。
27. C语言中const的使用方法
在 C 语言中,const
也是一个关键字,用于指定变量不可修改。const
可以应用于以下几个方面:
- 常量变量:使用
const
关键字可以声明一个常量变量,即变量的值不能被修改。例如:
const int MAX_SIZE = 100;
int arr[MAX_SIZE]; // 使用常量变量声明数组的大小
- 指针常量:使用
const
关键字可以将指针声明为常量,即指针指向的值不能被修改。例如:
int value = 42;
const int* ptr = &value; // 指针指向的值不能被修改
- 常量指针:使用
const
关键字可以将指针的指向声明为常量,即指针的指向不能被修改。例如:
int value = 42;
int* const ptr = &value; // 指针的指向不能被修改
需要注意的是,在 C 语言中没有像 C++ 中那样的常量成员函数的概念。如果需要实现类似的功能,可以使用函数指针来实现。例如:
struct MyClass {
int value;
};
typedef int (*GetValueFunc)(const struct MyClass* obj);
int MyClass_getValue(const struct MyClass* obj) {
// 实现常量成员函数的功能
return obj->value;
}
int main() {
struct MyClass obj = {42};
const struct MyClass* constObj = &obj;
GetValueFunc getValue = MyClass_getValue;
int value = getValue(constObj); // 调用常量成员函数
return 0;
}
在这个例子中,使用函数指针 GetValueFunc
来实现常量成员函数的功能。MyClass_getValue()
函数实现了常量成员函数的功能,即在函数内部不能修改类的成员变量。在 main()
函数中,使用函数指针 getValue
来调用常量成员函数,从而避免了修改类的成员变量。
总之,在 C 语言中,const
关键字可以用于常量变量、指针常量和常量指针等方面,但是没有常量成员函数的概念。如果需要实现类似的功能,可以使用函数指针来实现。
28. 哪些关键字可以修饰线程安全的变量
在 C++11 之后,可以使用 std::atomic
和 std::mutex
关键字来修饰线程安全的变量。
std::atomic
:使用原子类型可以保证对变量的读写操作是原子的,从而避免多个线程同时访问同一个变量时出现竞争条件。例如:
#include <atomic>
std::atomic<int> counter(0); // 声明一个原子类型变量
void incrementCounter() {
counter++; // 原子操作,保证对 counter 的读写操作是原子的
}
int main() {
// 创建多个线程并发执行 incrementCounter() 函数
// 确保多个线程同时访问 counter 变量
// 没有加锁也不会出现竞争条件
return 0;
}
在这个例子中,使用 std::atomic
关键字声明一个原子类型变量 counter
,并在多个线程中并发执行 incrementCounter()
函数,对 counter
变量进行读写操作。由于 counter
是原子类型,因此不需要加锁也可以保证对变量的读写操作是原子的,从而避免了竞争条件。
std::mutex
:使用互斥量可以保证多个线程对变量的访问是串行化的,从而避免竞争条件。例如:
#include <mutex>
std::mutex counterMutex; // 声明一个互斥量
int counter = 0; // 声明一个线程不安全的变量
void incrementCounter() {
counterMutex.lock(); // 加锁,保证只有一个线程可以访问 counter 变量
counter++;
counterMutex.unlock(); // 解锁
}
int main() {
// 创建多个线程并发执行 incrementCounter() 函数
// 确保多个线程同时访问 counter 变量
// 使用互斥量保证多个线程对变量的访问是串行化的
return 0;
}
在这个例子中,使用 std::mutex
关键字声明一个互斥量 counterMutex
,并在多个线程中并发执行 incrementCounter()
函数,对线程不安全的变量 counter
进行读写操作。由于使用了互斥量,保证了多个线程对变量的访问是串行化的,从而避免了竞争条件。
总之,可以使用 std::atomic
和 std::mutex
关键字来修饰线程安全的变量。使用原子类型可以保证对变量的读写操作是原子的,使用互斥量可以保证多个线程对变量的访问是串行化的,从而避免竞争条件
除了 std::atomic
和 std::mutex
,在 C++11 之后还有一些其他的关键字可以用来修饰线程安全的变量,包括:
std::atomic_flag
:std::atomic_flag
是一个特殊的原子类型,用于实现简单的自旋锁。它提供了两个成员函数test_and_set()
和clear()
,分别用于设置和清除标志位。例如:
#include <atomic>
std::atomic_flag flag = ATOMIC_FLAG_INIT; // 声明一个原子标志位
void lock() {
while (flag.test_and_set(std::memory_order_acquire)); // 自旋等待
}
void unlock() {
flag.clear(std::memory_order_release); // 清除标志位
}
int main() {
// 创建多个线程并发执行 lock() 和 unlock() 函数
// 使用原子标志位实现自旋锁
return 0;
}
在这个例子中,使用 std::atomic_flag
关键字声明一个原子标志位 flag
,并在多个线程中并发执行 lock()
和 unlock()
函数,使用原子标志位实现自旋锁。
std::shared_mutex
:std::shared_mutex
是一个读写锁,允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。例如:
#include <shared_mutex>
std::shared_mutex mutex; // 声明一个读写锁
int counter = 0; // 声明一个线程不安全的变量
void incrementCounter() {
std::unique_lock<std::shared_mutex> lock(mutex); // 加写锁
counter++;
}
int getCounter() {
std::shared_lock<std::shared_mutex> lock(mutex); // 加读锁
return counter;
}
int main() {
// 创建多个线程并发执行 incrementCounter() 和 getCounter() 函数
// 使用读写锁保证多个线程对变量的访问是安全的
return 0;
}
在这个例子中,使用 std::shared_mutex
关键字声明一个读写锁 mutex
,并在多个线程中并发执行 incrementCounter()
和 getCounter()
函数,使用读写锁保证多个线程对变量的访问是安全的。
总之,除了 std::atomic
和 std::mutex
,还可以使用 std::atomic_flag
和 std::shared_mutex
等关键字来修饰线程安全的变量。不同的关键字适用于不同的场景,需要根据具体情况选择合适的关键字。
29. C++编译器会给一个空类自动生成哪些函数
在 C++ 中,空类指的是没有任何成员变量和成员函数的类。对于空类,编译器会自动生成以下函数:
- 默认构造函数(Default Constructor):空类的默认构造函数被自动生成,即使我们没有显式地声明它。默认构造函数没有参数,也没有函数体,它的作用是创建一个空类的对象。例如:
class EmptyClass {
// 空类,没有任何成员变量和成员函数
};
int main() {
EmptyClass obj; // 调用默认构造函数创建一个 EmptyClass 的对象
return 0;
}
在这个例子中,我们声明了一个空类 EmptyClass
,并在 main()
函数中创建一个 EmptyClass
对象 obj
,编译器会自动生成默认构造函数。
- 拷贝构造函数(Copy Constructor):空类的拷贝构造函数也被自动生成。拷贝构造函数用于在创建一个对象时,以另一个对象作为初始值。对于空类,拷贝构造函数没有参数,也没有函数体,它的作用是创建一个与另一个空类对象一样的新对象。例如:
class EmptyClass {
// 空类,没有任何成员变量和成员函数
};
int main() {
EmptyClass obj1;
EmptyClass obj2(obj1); // 调用拷贝构造函数创建一个与 obj1 相同的 EmptyClass 对象 obj2
return 0;
}
在这个例子中,我们声明了一个空类 EmptyClass
,并在 main()
函数中创建了两个 EmptyClass
对象 obj1
和 obj2
,其中 obj2
是通过调用拷贝构造函数生成的一个与 obj1
相同的新对象。
- 拷贝赋值运算符(Copy Assignment Operator):空类的拷贝赋值运算符也被自动生成。拷贝赋值运算符用于将一个对象的值赋给另一个对象。对于空类,拷贝赋值运算符没有参数,也没有函数体,它的作用是将一个空类对象的值赋给另一个空类对象。例如:
class EmptyClass {
// 空类,没有任何成员变量和成员函数
};
int main() {
EmptyClass obj1;
EmptyClass obj2;
obj2 = obj1; // 调用拷贝赋值运算符将 obj1 的值赋给 obj2
return 0;
}
在这个例子中,我们声明了一个空类 EmptyClass
,并在 main()
函数中创建了两个 EmptyClass
对象 obj1
和 obj2
,其中 obj2
是通过调用拷贝赋值运算符将 obj1
的值赋给的一个新对象。
- 析构函数(Destructor):空类的析构函数也被自动生成。析构函数用于在对象生命周期结束时,释放对象占用的资源。对于空类,析构函数没有参数,也没有函数体,它的作用是释放一个空类对象占用的资源。例如:
class EmptyClass {
// 空类,没有任何成员变量和成员函数
};
int main() {
EmptyClass obj; // 调用默认构造函数创建一个 EmptyClass 的对象
// 对象生命周期结束,调用析构函数释放占用的资源
return 0;
}
在这个例子中,我们声明了一个空类 EmptyClass
,并在 main()
函数中创建了一个 EmptyClass
对象 obj
,对象生命周期结束时,编译器会自动调用析构函数释放占用的资源。
总之,对于空类,编译器会自动生成默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。这些函数都没有参数,也会生成析构函数。
30. Switch语句的case里面为何不能定义变量
在 C++ 中,switch
语句的 case
标签里不能定义变量,因为 case
标签只是一条标签语句,不是一个作用域。当程序执行到某个 case
标签时,程序会跳转到该标签对应的代码块执行,但是在执行该代码块之前,不会创建新的作用域。
因此,在 case
标签里定义的变量会影响到前面的代码,也会影响到后面的代码。例如:
#include <iostream>
int main() {
int x = 1;
switch (x) {
case 1:
int y = 2; // 错误:在 case 标签里定义变量
std::cout << "y = " << y << std::endl; // 输出 2
break;
case 2:
std::cout << "y = " << y << std::endl; // 错误:y 未定义
break;
default:
std::cout << "y = " << y << std::endl; // 错误:y 未定义
break;
}
return 0;
}
在这个例子中,我们在 case
标签 1
里定义了一个变量 y
,并在该标签对应的代码块里使用了它。然而,当程序执行到 case
标签 2
或 default
时,变量 y
已经超出了作用域,因此在这些代码块里不能使用它。
为了避免这种问题,应该在 switch
语句外部定义变量,并在 case
标签里使用这些变量。例如:
#include <iostream>
int main() {
int x = 1;
int y;
switch (x) {
case 1:
y = 2; // 在当前作用域里赋值
std::cout << "y = " << y << std::endl; // 输出 2
break;
case 2:
std::cout << "y = " << y << std::endl; // 正确:使用已定义的变量 y
break;
default:
std::cout << "y = " << y << std::endl; // 正确:使用已定义的变量 y
break;
}
return 0;
}
在这个例子中,我们在 switch
语句外部定义了变量 y
,并在 case
标签里使用它。这样,我们就可以避免在 case
标签里定义变量而导致的作用域问题。
31. include " " 和 include <>的区别
在 C++ 中,#include
是一个预处理指令,用于包含外部文件或库的头文件。#include
指令有两种格式:
#include "filename"
#include <filename>
这两种格式有以下区别:
-
#include "filename"
:用于包含用户自定义的头文件。编译器首先在当前文件所在目录中查找头文件,如果找不到,再在系统预定义的目录中查找。这种格式的#include
指令通常用于包含自己编写的头文件。 -
#include <filename>
:用于包含系统预定义的头文件。编译器只在系统预定义的目录中查找头文件。这种格式的#include
指令通常用于包含标准库的头文件。
举个例子,假设我们有一个头文件 myheader.h
,保存在当前文件所在目录中。假设我们还有一个标准库头文件 iostream
,保存在系统预定义的目录中。
如果我们想在一个源文件中包含这两个头文件,可以使用以下代码:
#include "myheader.h" // 包含自定义的头文件
#include <iostream> // 包含标准库的头文件
这样,编译器会先在当前文件所在目录中查找 myheader.h
,如果找到了就包含它;然后在系统预定义的目录中查找 iostream
,如果找到了就包含它。
总之,#include "filename"
和 #include <filename>
的区别在于编译器查找头文件的路径不同。如果头文件是自己编写的,应该使用 #include "filename"
;如果头文件是系统预定义的,应该使用 #include <filename>
。
32. define和typedef的区别
#define
和 typedef
都是 C++ 中用于定义类型别名的关键字,但它们的作用和使用方式有所不同。
-
#define
:用于定义宏,可以用来定义常量、函数、类型等。#define
宏定义是在预处理阶段进行的,它会在源代码中将定义的标识符替换为指定的文本。例如,下面的代码使用#define
定义了一个常量:#define PI 3.14159
在预处理阶段,编译器会将代码中的
PI
替换为3.14159
。 -
typedef
:用于定义类型别名,可以用来为已有的类型定义一个新名字。typedef
类型别名定义是在编译阶段进行的,它不会替换任何文本。例如,下面的代码使用typedef
定义了一个新的类型别名:typedef int myint;
在编译阶段,
myint
将作为int
的别名使用,可以用来声明变量、参数、返回值等。
因此,#define
和 typedef
的区别在于:
#define
可以定义常量、函数、类型等,而typedef
只能定义类型别名。#define
在预处理阶段进行宏替换,而typedef
在编译阶段进行类型别名定义。#define
定义的标识符会被替换为指定的文本,而typedef
定义的别名不会替换任何文本。
总之,#define
和 typedef
都是用于定义类型别名的关键字,但它们的作用和使用方式有所不同,根据具体的需求选择合适的关键字。一般来说,如果要定义常量、函数、类型等,应该使用 #define
;如果要为已有的类型定义一个新名字,应该使用 typedef
。
33. 深拷贝和浅拷贝的区别
在 C++ 中,拷贝是指将一个对象的值复制到另一个对象中。拷贝可以分为深拷贝和浅拷贝两种。
-
浅拷贝(Shallow Copy):将一个对象的成员变量的值复制到另一个对象中。浅拷贝只复制指针或引用地址,而不是复制指针或引用所指向的对象。因此,多个对象的指针或引用指向同一个对象。例如:
class Person { public: int age; char* name; }; Person p1; p1.age = 20; p1.name = new char[10]; strcpy(p1.name, "Tom"); Person p2 = p1; // 浅拷贝 p2.age = 30; strcpy(p2.name, "Jerry"); cout << p1.age << " " << p1.name << endl; // 输出 20 Jerry cout << p2.age << " " << p2.name << endl; // 输出 30 Jerry
在上面的例子中,我们定义了一个
Person
类,它有两个成员变量age
和name
。我们创建了两个Person
类对象p1
和p2
,并且将p1
的值复制到p2
中。由于p1
和p2
的成员变量name
指向同一个字符串对象,所以当我们修改p2.name
的值时,p1.name
的值也会改变。 -
深拷贝(Deep Copy):将一个对象的值以及它所指向的对象的值都复制到另一个对象中。深拷贝会创建一个新的对象,而不是共享已有的对象。例如:
class Person { public: int age; char* name; Person(const Person& other) { // 拷贝构造函数 age = other.age; name = new char[strlen(other.name) + 1]; strcpy(name, other.name); } ~Person() { // 析构函数 if (name != nullptr) { delete[] name; name = nullptr; } } }; Person p1; p1.age = 20; p1.name = new char[10]; strcpy(p1.name, "Tom"); Person p2 = p1; // 深拷贝 p2.age = 30; strcpy(p2.name, "Jerry"); cout << p1.age << " " << p1.name << endl; // 输出 20 Tom cout << p2.age << " " << p2.name << endl; // 输出 30 Jerry
在上面的例子中,我们为
Person
类定义了一个拷贝构造函数和一个析构函数。在拷贝构造函数中,我们使用new
运算符创建一个新的字符串对象,并将原对象的字符串值复制到新对象中。在析构函数中,我们使用delete
运算符释放字符串对象的内存空间。当我们将p1
的值复制到p2
中时,会调用Person
类的拷贝构造函数,从而创建一个新的字符串对象,并将p1.name
的值复制到新对象中。因此,当我们修改p2.name
的值时,p1.name
的值不会改变。
总之,浅拷贝只复制指针或引用地址,而不是复制指针或引用所指向的对象,因此多个对象的指针或引用指向同一个对象,如果其中一个对象被修改,其他对象也会受到影响。而深拷贝会创建一个新的对象,并将原对象的值以及它所指向的对象的值都复制到新对象中,因此每个对象都有自己的副本,修改一个对象不会影响其他对象。