【C++八股-第八期】类 - 24年春招特供
感谢关注,你必OC
提纲:
👉 八股:
简单解释一下深拷贝和浅拷贝的区别
请比较C++结构体和C结构体的区别
C++中struct和class的区别
请说明在C++中类的访问权限有几种
介绍一下this指针
静态成员函数与普通成员函数的区别
能调用类初始化为nullptr的成员函数吗?如果可行讲解运行机制和潜在问题
为什么静态成员函数不能访问非静态成员
使用对象作为参数时 使用 值传递 还是 引用传递
拷贝构造函数的参数类型是什么?为什么?
判断构造次数和析构次数
介绍一下初始化列表
实现一个string类
👉 代码:
1. 简单解释一下深拷贝和浅拷贝的区别
-
内存共享:
-
浅拷贝:共享相同的内存,修改一个对象的内存内容会影响到另一个对象。
-
深拷贝:拥有各自独立的内存,修改一个对象的内存内容不会影响到另一个对象。
-
-
适用场景:
-
浅拷贝适用于对象内部没有动态分配内存或者可以共享相同内存的情况。
-
深拷贝适用于对象内部有动态分配内存且需要独立拷贝的情况。
-
浅拷贝(Shallow Copy):
定义: 浅拷贝是将一个对象的内容复制到另一个对象,但不复制对象内部的动态分配内存。两个对象共享相同的内存块,包括指针成员指向的动态分配内存。
内存共享: 新旧对象共享相同的动态分配内存,修改一个对象的内存内容会影响到另一个对象。
示例:
class ShallowCopy { public: int* data; ShallowCopy(const ShallowCopy& other) { data = other.data; // 浅拷贝,共享内存 } }; ShallowCopy obj1; obj1.data = new int; ShallowCopy obj2 = obj1; // 浅拷贝,obj2.data 指向 obj1.data
深拷贝(Deep Copy):
定义: 深拷贝是将一个对象的内容复制到另一个对象,并且复制对象内部的动态分配内存。两个对象拥有各自独立的内存块,修改一个对象的内存内容不会影响到另一个对象。
内存独立: 新旧对象拥有各自独立的动态分配内存,修改一个对象的内存内容不会影响到另一个对象。
示例:
class DeepCopy { public: int* data; DeepCopy(const DeepCopy& other) { data = new int(*other.data); // 深拷贝,分配新的内存 } ~DeepCopy() { delete data; } }; DeepCopy obj1; obj1.data = new int; DeepCopy obj2 = obj1; // 深拷贝,obj2.data 指向新的内存
2. 请比较C++结构体和C结构体的区别
-
成员访问权限:
- C 结构体: 所有成员都是公有的,默认的访问权限是公有的。
struct Point { int x; int y; };
- C++ 结构体: 可以包含公有成员和私有成员,也可以包含构造函数、析构函数等。
struct Point { int x; // 默认 public int y; // 默认 public private: int z; // 私有成员 };
-
成员函数:
-
C 结构体: 不支持在结构体中定义成员函数。
-
C++ 结构体: 可以包含成员函数,允许对结构体执行操作。
struct Point { int x; int y; void print() { cout << "(" << x << ", " << y << ")" << endl; } };
-
-
默认访问控制:
-
C 结构体: 默认成员的访问控制是公有的。
-
C++ 结构体: 默认成员的访问控制是公有的,但可以使用 class 关键字定义类,成员的默认访问控制是私有的。
-
-
默认初始化:
-
C 结构体: 不支持成员的默认初始化。
-
C++ 结构体: 可以使用默认成员初始化来初始化成员变量。
struct Point { int x = 0; // C++11 起支持成员变量的默认初始化 int y = 0; };
-
-
继承:
-
C 结构体: 不支持继承。
-
C++ 结构体: 可以通过继承其他结构体或类来拓展。
struct ColorPoint : Point { string color; };
-
-
构造函数和析构函数:
-
C 结构体: 不支持构造函数和析构函数。
-
C++ 结构体: 可以包含构造函数和析构函数,支持对象的初始化和清理。
struct Point { int x; int y; Point(int x, int y) : x(x), y(y) {} // 构造函数 ~Point() {} // 析构函数 };
-
3. C++中struct和class的区别
-
struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装;
-
struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的
-
在继承关系中,struct 默认是公有继承,而 class 是私有继承; class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数,
template<typename T, typename Y> // 可以把typename 换成 class int Func(const T& t, const Y& y) { //TODO }
4. 请说明在C++中类的访问权限有几种
- 公有(public)访问权限:
在公有访问权限下,类的成员对外部代码是可见且可访问的。这意味着其他类和函数可以直接访问公有成员。
class MyClass {
public:
int publicMember; // 公有成员
void publicFunction() {
// 公有函数
}
};
- 私有(private)访问权限:
在私有访问权限下,类的成员对外部代码是不可见的。只有类的内部成员函数可以直接访问私有成员。
class MyClass {
private:
int privateMember; // 私有成员
void privateFunction() {
// 私有函数
}
};
- 保护(protected)访问权限:
保护访问权限介于公有和私有之间。与私有成员一样,在类外部是不可见的,但在派生类中是可见的。通常用于实现继承中的封装。
class Base {
protected:
int protectedMember; // 保护成员
};
class Derived : public Base {
public:
void accessProtectedMember() {
protectedMember = 10; // 在派生类中可以访问保护成员
}
};
5. 介绍一下this指针
在 C++ 中,this
指针是一个特殊的指针,它是一个隐式参数,指向当前对象的地址。this
指针在成员函数内部自动存在,用于引用调用该函数的对象。this指针在成员函数的开始执行前构造的,在成员的执行结束后清除。
-
在成员函数中使用: 在类的成员函数中,可以使用 this 指针来引用对象的成员变量和成员函数。this指针只有在成员函数中才有定义,创建一个对象后,不能通过对象使用this指针。也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。
class MyClass { public: int x; void setX(int value) { this->x = value; // 使用this指针引用成员变量 } void printX() { cout << "x: " << this->x << endl; // 使用this指针引用成员变量 } };
-
用于解决名称冲突: 在成员函数中,如果有局部变量和成员变量同名,使用 this 指针可以明确指出使用的是成员变量。
class MyClass { private: int x; public: void setX(int x) { this->x = x; // 使用this指针解决名称冲突 } };
-
作为返回值: 在成员函数中返回对象自身,可以使用
return *this;
这在实现链式调用时很有用。class MyClass { public: MyClass& increment() { x++; return *this; // 返回对象自身,支持链式调用 } private: int x; };
-
不适用于静态成员函数: this 指针只能在非静态成员函数中使用,因为静态成员函数没有隐式的对象参数。
class Point { private: int x; int y; public: Point(int x, int y) : x(x), y(y) {} // 成员函数中使用this指针 void print() { cout << "x: " << this->x << ", y: " << this->y << endl; } // 链式调用 Point& setX(int newX) { this->x = newX; return *this; } }; int main() { Point p(1, 2); p.print(); // 输出 x: 1, y: 2 p.setX(10).print(); // 输出 x: 10, y: 2,支持链式调用 return 0; }
6. 静态成员函数与普通成员函数的区别
-
关联对象: 静态成员函数与类本身关联,不依赖于具体的对象实例;普通成员函数依赖于特定的对象实例。
-
访问权限: 静态成员函数只能访问类的静态成员;普通成员函数可以访问类的所有成员。
-
调用方式: 静态成员函数可以直接通过类名调用,而普通成员函数必须通过对象实例调用。
-
内存占用: 静态成员函数不包含隐式的对象指针(this 指针),因此在内存上效率可能更高一些。
示例:
class MyClass {
public:
static void staticFunction() {
cout << "This is a static member function." << endl;
}
void nonStaticFunction() {
cout << "This is a non-static member function." << endl;
}
static int staticVariable;
int nonStaticVariable;
};
int MyClass::staticVariable = 0;
int main() {
MyClass::staticFunction(); // 静态成员函数调用
MyClass obj;
obj.nonStaticFunction(); // 普通成员函数调用
return 0;
}
拓展(了解即可):
静态成员函数(Static Member Functions):
声明和定义: 静态成员函数使用 static 关键字进行声明和定义。
访问权限: 静态成员函数可以访问类的静态成员和其他静态成员函数,但不能访问非静态成员和非静态成员函数。它们不依赖于特定的对象,因此不能访问对象的非静态成员。
调用方式: 可以通过类名直接调用静态成员函数,无需创建对象实例。
MyClass::staticMemberFunction(); // 直接使用类名调用
普通成员函数(Non-static Member Functions):
声明和定义: 普通成员函数没有 static 关键字,是类的成员函数的默认类型。
访问权限: 普通成员函数可以访问类的所有成员变量和成员函数,包括静态和非静态成员。它们依赖于特定的对象实例,可以访问对象的所有成员。
调用方式: 必须通过对象实例来调用普通成员函数。
MyClass obj; obj.nonStaticMemberFunction(); // 通过对象实例调用
7. 能调用类初始化为nullptr的成员函数吗?如果可行讲解运行机制和潜在问题
可以使用类对象的指针成员初始化为 nullptr,然后调用成员函数。这通常发生在类的构造函数中,或者在类的成员函数中对指针成员进行初始化。不使用其中的 this 指针即可
class animal{
public:
void create(){ cout << "animal create\n"; }
};
int main(){
animal *a = nullptr;
a->create(); //输出 animal create
return 0;
}
8. 为什么静态成员函数不能访问非静态成员
静态成员函数不能访问非静态成员,因为静态成员函数与特定的对象实例无关,它不包含隐式的对象指针(this 指针)。非静态成员属于特定的对象实例,它们依赖于对象的存在,因此需要通过对象实例来访问。
class MyClass {
private:
int x; // 非静态成员变量
public:
static void staticFunction() {
x = 10; // 错误!无法访问非静态成员变量x
}
void nonStaticFunction() {
x = 10; // 正确!可以访问非静态成员变量x
}
};
9. 使用对象作为参数时 使用 值传递 还是 引用传递
-
如果函数需要修改原始对象或者操作对象的状态,通常使用引用传递更加高效和合适。
-
如果函数不需要修改原始对象,并且对象较小,可以考虑使用值传递。
-
对于大型对象,通常建议使用引用传递,以避免不必要的复制开销。
-
如果不想修改原始对象,可以使用 const 引用传递对象,以提高安全性。
值传递(Pass by Value):
特点: 将对象的副本传递给函数,函数操作的是对象的副本,而不是原始对象本身。
优点:
简单易懂:传递对象的副本,不会影响原始对象。
安全性:避免了原始对象被意外修改的风险。
缺点:
- 开销较大:如果对象较大,值传递会消耗更多的内存和时间来复制对象。
*性能损失:复制大对象可能会影响程序的性能。
void functionByValue(MyClass obj) { // 对象obj的副本被传递给函数 }
引用传递(Pass by Reference):
特点: 将对象的引用传递给函数,函数直接操作原始对象。
优点:
省略了对象的复制过程,节省了内存和时间。
可以直接修改原始对象,避免了副本的创建。
缺点:
- 容易产生副作用:函数可以直接修改原始对象,可能导致意外的副作用。
- 需要谨慎使用:如果不希望修改原始对象,应该使用 const 引用来传递对象。
void functionByReference(MyClass& obj) { // 对象obj的引用被传递给函数,直接操作原始对象 }
10. 拷贝构造函数的参数类型是什么?为什么?
MyClass(const MyClass& other)
-> 参数类型是常量引用(const reference)
拷贝构造函数用于创建一个对象的副本,其参数表示需要被拷贝的原始对象。为了避免在拷贝构造函数中产生额外的拷贝,参数通常使用常量引用。使用常量引用的好处包括:
-
避免拷贝: 如果参数是非常量引用,传递给拷贝构造函数的参数会生成原始对象的一个副本,这会导致额外的拷贝操作,增加了开销。使用常量引用可以避免这种额外的拷贝,因为它不会创建临时对象。
-
防止修改原始对象: 使用常量引用还可以防止在拷贝构造函数中意外修改原始对象。因为常量引用不能修改原始对象,所以拷贝构造函数的参数声明为常量引用可以提供更好的安全性。
-
防止会造成无穷递归: 如果拷贝构造函数中的参数不是一个引用,即形如
MyClass(const MyClass other)
,那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。
11. 判断构造次数和析构次数
class Counter {
private:
static int count; // 静态成员变量,用于记录对象数量
public:
// 构造函数,用于增加构造次数
Counter() {
count++;
std::cout << "Constructed. Count = " << count << std::endl;
}
// 析构函数,用于增加析构次数
~Counter() {
count--;
std::cout << "Destructed. Count = " << count << std::endl;
}
// 静态成员函数,用于获取对象数量
static int getCount() {
return count;
}
};
// 初始化静态成员变量
int Counter::count = 0;
判断构造次数和析构次数:
int main() {
Counter obj1; // 构造次数+1 Constructed. Count = 1
Counter obj2; // 构造次数+1 Constructed. Count = 2
{
Counter obj3; // 构造次数+1 Constructed. Count = 3
} // obj3超出作用域,析构次数+1 Destructed. Count = 2
Counter obj4; // 构造次数+1 Constructed. Count = 3
return 0;
// Destructed. Count = 2
// Destructed. Count = 1
// Destructed. Count = 0
}
12. 介绍一下初始化列表
foo(string s, int i):name(s), id(i){}
初始化列表(Initialization List) 是在 C++ 中用于初始化成员变量的一种机制。它在对象构造函数的参数列表后面通过冒号(:)进行声明,并在冒号后面用逗号分隔各个成员变量的初始化。
优点:
-
效率: 使用初始化列表可以直接初始化成员变量,而不是先调用默认构造函数再赋值,这样可以提高代码的效率。
-
避免二次赋值: 使用初始化列表可以避免对成员变量进行两次赋值,首先是在构造函数体内赋值,然后在初始化列表中再次赋值。
-
必须初始化常量成员: 对于类中的常量成员变量(即用 const 关键字修饰的成员变量),它们只能在初始化列表中进行初始化,无法在构造函数体内进行赋值。
-
初始化顺序: 初始化列表中成员变量的初始化顺序与它们在类中的声明顺序相同,而不是按照初始化列表中的顺序。
class MyClass {
private:
int x;
int y;
public:
// 构造函数使用初始化列表进行成员变量的初始化
MyClass(int a, int b) : x(a), y(b);
};
拓展: -> 使用场景
成员变量需要通过构造函数进行初始化: 当类的成员变量需要在构造函数中进行初始化时,初始化列表是更好的选择。特别是对于类类型的成员变量,通过初始化列表可以直接调用其构造函数进行初始化,而不是先调用默认构造函数再进行赋值。
// 定义一个简单的类 Address class Address { private: std::string city; std::string street; public: // 构造函数使用初始化列表初始化成员变量 Address(const std::string& c, const std::string& s) : city(c), street(s) {} // 成员函数,用于输出地址信息 void display() { std::cout << "City: " << city << ", Street: " << street << std::endl; } }; // 定义一个类 Person,包含一个 Address 类型的成员变量 class Person { private: std::string name; int age; Address address; // 类类型的成员变量 public: // 构造函数使用初始化列表初始化成员变量 Person(const std::string& n, int a, const std::string& c, const std::string& s) : name(n), age(a), address(c, s) {} // 成员函数,用于输出人员信息 void display() { std::cout << "Name: " << name << ", Age: " << age << std::endl; address.display(); // 调用 Address 类的成员函数显示地址信息 } }; int main() { // 创建 Person 对象时,通过初始化列表初始化成员变量 Person p("Alice", 30, "New York", "Main Street"); p.display(); // 输出人员信息 return 0; }
提高效率和性能: 使用初始化列表可以 避免在构造函数体内进行赋值操作,从而提高代码的效率和性能 。尤其是对于大型对象或需要频繁创建的对象,初始化列表可以显著减少不必要的开销。
class Data { private: std::vector<int> data; // 大型对象,存储大量数据 public: // 构造函数使用初始化列表初始化成员变量 Data() : data(1000000, 0) {} // 初始化 data 向量,包含1000000个元素,初始值为0 // 成员函数,用于获取数据的大小 int size() const { return data.size(); } };
必须初始化常量成员: 类中的常量成员变量(如使用 const 修饰的成员变量)只能在初始化列表中进行初始化,无法在构造函数体内进行赋值。因此,当需要初始化常量成员变量时,必须使用初始化列表。
class Circle { private: const double pi; //常量成员 double radius; public: // 使用初始化列表初始化常量成员变量 Circle(double r) : pi(3.14159), radius(r) {} // 成员函数,用于计算圆的面积 double area() { return pi * radius * radius; } }; int main() { Circle c(5.0); // 创建 Circle 对象时使用初始化列表初始化常量成员变量 std::cout << "Area of the circle: " << c.area() << std::endl; // 输出圆的面积 return 0; }
提高代码可读性和可维护性: 使用初始化列表可以更清晰地表达对象的初始化过程,使代码更具可读性和可维护性。初始化列表明确地将成员变量的初始化与构造函数的实现分开,使代码结构更清晰。
13. 实现一个string类
#include <iostream>
#include <cstring>
using namespace std;
class String{
public:
// 默认构造函数
String(const char *str = nullptr);
// 拷贝构造函数
String(const String &str);
// 析构函数
~String();
// 字符串赋值函数
String& operator=(const String &str);
private:
char *m_data;
int m_size;
};
// 构造函数
String::String(const char *str)
{
if(str == nullptr) // 加分点:对m_data加NULL 判断
{
m_data = new char[1]; // 得分点:对空字符串自动申请存放结束标志'\0'的
m_data[0] = '\0';
m_size = 0;
}
else
{
m_size = strlen(str);
m_data = new char[m_size + 1];
strcpy(m_data, str);
}
}
// 拷贝构造函数
String::String(const String &str) // 得分点:输入参数为const型
{
m_size = str.m_size;
m_data = new char[m_size + 1]; //加分点:对m_data加NULL 判断
strcpy(m_data, str.m_data);
}
// 析构函数
String::~String()
{
delete[] m_data;
}
// 字符串赋值函数
String& String::operator=(const String &str) // 得分点:输入参数为const
{
if(this == &str) //得分点:检查自赋值
return *this;
delete[] m_data; //得分点:释放原有的内存资源
m_size = strlen(str.m_data);
m_data = new char[m_size + 1]; //加分点:对m_data加NULL 判断
strcpy(m_data, str.m_data);
return *this; //得分点:返回本对象的引用
}