1. 指针和引用的区别
定义与初始化:
指针:可以声明时不初始化,并且可以在之后指向任何同类型的变量。指针是一个变量,它存储的是另一个变量的地址。
int a = 10;
int* p; // 声明一个指向int的指针
p = &a; // 将p指向变量a的地址
引用:一旦被创建,就必须初始化,并且从一而终地引用同一个对象。引用更像是已有变量的一个别名。
int a = 10;
int& ref = a; // 创建一个对a的引用
重定向:
指针:可以改变指针所指向的对象,即可以重新赋值给指针使其指向不同的内存地址。
int b = 20;
p = &b; // 现在p指向了b
引用:一旦引用被初始化为一个对象,就不能再指向其他对象。
内存地址操作:
指针:可以通过*(解引用运算符)访问或修改指针指向的值,使用&获取指针本身的地址。
引用:不能获取引用的地址,也不能创建指向引用的指针。引用直接作用于其绑定的对象。
空值:
指针:可以具有nullptr值,表示不指向任何有效的内存地址。
引用:必须总是引用某个有效的对象,不能是null。
2 . new / delete 与 malloc / free的异同
相同点
内存分配:
两者都可以用来动态地从堆(Heap)中分配内存。
手动管理:
都需要程序员手动释放分配的内存以避免内存泄漏。
不同点
1. 语法与类型安全
new/delete:
new 是一个操作符,它不仅分配内存,还会调用对象的构造函数来初始化新创建的对象。
delete 也会调用对象的析构函数,然后释放内存。
支持类型安全的内存分配和释放,能够自动计算所需分配的字节数。MyClass* obj = new MyClass(); // 分配并构造对象
delete obj; // 调用析构函数并释放内存
malloc/free:
malloc 和 free 是标准库函数,只负责分配和释放内存块,不会调用任何构造函数或析构函数。
返回的是 void* 类型,需要显式转换为正确的类型,并且需要手动计算所需分配的字节数。MyClass* obj = (MyClass*) malloc(sizeof(MyClass)); // 只分配内存
if(obj != nullptr) {new(obj) MyClass(); // 使用placement new调用构造函数
}
free(obj); // 只释放内存,不调用析构函数
2. 构造与析构
new/delete:
自动调用对象的构造函数和析构函数。
malloc/free:
不会自动调用构造函数或析构函数。如果需要,可以使用“placement new”来手动调用构造函数,并手动调用析构函数。
3. 错误处理
new:
如果内存分配失败,new 会抛出 std::bad_alloc 异常(除非使用了 nothrow 版本的 new,这种情况下会返回 nullptr)。MyClass* obj = new(std::nothrow) MyClass();
if(obj == nullptr) {// 处理内存不足的情况
}
malloc:如果内存分配失败,malloc 返回 nullptr,因此需要检查返回值是否为 nullptr 来判断分配是否成功。
变量声明和定义区别?
变量声明(Declaration)
声明是指告诉编译器某个变量的名字及其类型,但不一定为该变量分配存储空间。声明的主要目的是使编译器知道该变量的存在,以便在代码中可以使用它。通常,声明出现在头文件(.h 或 .hpp)中,或者在一个源文件中引用另一个源文件中的变量时使用。
特点:
告知编译器:声明只是告诉编译器这个变量存在,并指定了它的类型。
不分配内存:通常不会为声明的变量分配实际的存储空间(除非是外部链接的情况,如extern关键字)。
允许多次声明:同一个变量可以在多个地方声明,只要这些声明一致即可。
示例:
// 在头文件或源文件中声明一个外部变量
extern int globalVar; // 声明了一个名为globalVar的int型变量,但未定义它
变量定义(Definition)
定义不仅包括声明的所有信息,还包括为变量分配实际的存储空间。定义确保了变量在程序运行时有一个具体的内存位置来存储其值。
特点:
分配内存:定义会为变量分配实际的存储空间。
初始化可选:可以在定义时对变量进行初始化。
只能定义一次:每个变量只能被定义一次,否则会导致重复定义错误。
示例:
// 在源文件中定义并初始化一个全局变量
int globalVar = 10; // 定义了一个名为globalVar的int型变量,并分配了存储空间// 在函数内部定义局部变量
void someFunction() {int localVar = 5; // 定义并初始化了一个局部变量
}
结合使用
在实际编程中,声明和定义经常结合使用,尤其是在大型项目中,通过头文件声明变量并在相应的源文件中定义它们,可以实现模块化设计。
示例:
假设你有一个项目包含两个文件:main.cpp 和 globals.h。
globals.h 文件内容:
#ifndef GLOBALS_H
#define GLOBALS_Hextern int globalVar; // 声明一个外部变量#endif
main.cpp 文件内容:
#include "globals.h"int globalVar = 20; // 定义并初始化全局变量int main() {// 使用globalVarreturn 0;
}
在这个例子中,globals.h 中仅声明了 globalVar,而 main.cpp 中则定义并初始化了它。这样做的好处是可以将变量的声明与定义分离,便于管理和维护大型项目。
指针常量和常量指针的区别
1. 指针常量(Pointer Constant)
指针常量是指一个指针本身是常量,即该指针的地址值不能改变,但可以通过该指针修改其所指向的内容。
特点:
- 指针本身的地址值是固定的,一旦初始化后就不能再指向其他地址。
- 可以通过该指针修改其所指向的对象的值。
定义与使用:
int a = 10;
int b = 20;
// 定义一个指针常量,并初始化为指向a
int* const p = &a; // p是一个指向int的常量指针
*p = 30; // 合法:可以通过p修改a的值
// p = &b; // 非法:不能改变p所指向的地址
2. 常量指针(Constant Pointer)
常量指针是指一个指针指向的内容是常量,即不能通过该指针修改其所指向的对象的值,但指针本身可以指向其他地址。
特点:
- 不能通过该指针修改其所指向的对象的值。
- 指针本身可以重新赋值,指向其他地址。
定义与使用:
int a = 10;
int b = 20;
// 定义一个常量指针,并初始化为指向a
const int* p = &a; // p是一个指向常量int的指针
// *p = 30; // 非法:不能通过p修改a的值
p = &b; // 合法:可以改变p所指向的地址
C++ 中 const
和 static
的作用
在C++中,const
和 static
是两个非常重要的关键字,它们各自有不同的用途和作用。理解它们的作用可以帮助你编写更高效、安全和模块化的代码。
const
关键字
const
用于声明常量,即其值不能被修改的变量或对象。它可以应用于不同的上下文,如变量、函数参数、返回值、成员函数等。
主要用途:
1. 常量变量
声明一个常量变量,一旦初始化后就不能再改变其值。
const int MAX_SIZE = 100;
2. 常量指针
指向常量数据的指针,或者指针本身是常量。
const int* p1; // 指向常量数据的指针
int* const p2 = &x; // 指针常量,指向不可变地址
const int* const p3 = &x; // 常量指针指向常量数据
3. 常量成员函数
成员函数标记为 表示该函数不会修改类的任何成员变量(除非这些变量被声明为 mutable
)。
class MyClass {
public:int getValue() const { return value; } // 常量成员函数
private:int value;
};
4. 函数参数和返回值
函数参数或返回值可以声明为 const
,以确保不会意外修改传入的数据。
void process(const std::string& str) { /* 使用str但不修改它 */ }
const std::string& getName() const { return name; }
static
关键字
static
关键字有多种用途,具体取决于它应用的上下文。它可以用于全局变量、局部变量、函数、类成员变量和成员函数。
主要用途:
1. 静态全局变量
在文件范围内声明的静态变量只能在该文件内访问,无法从其他文件访问。
static int globalVar = 10; // 只能在当前文件中访问
2. 静态局部变量
局部静态变量在第一次进入作用域时初始化,并且在整个程序生命周期内保持其值,即使离开该作用域也不会销毁。
void foo() {static int count = 0; // 只初始化一次++count;std::cout << "Count: " << count << std::endl;
}
3. 静态成员变量
类的静态成员变量属于类本身而不是某个特定的对象实例,所有对象共享同一个静态成员变量。
class MyClass {
public:static int sharedValue; // 静态成员变量声明
};int MyClass::sharedValue = 0; // 定义和初始化
4. 静态成员函数
类的静态成员函数属于类本身,可以通过类名直接调用,不需要创建类的实例。静态成员函数只能访问静态成员变量和其他静态成员函数。
class MyClass {
public:static void doSomething() { /* 静态成员函数 */ }
};MyClass::doSomething(); // 直接通过类名调用
5. 静态函数
在文件范围内声明的静态函数只能在该文件内访问,无法从其他文件访问。
static void helperFunction() { /* 仅在当前文件中可见 */ }
总结
const
:
- 用于定义常量,确保某些数据或行为在程序运行期间不会被修改。
- 提高代码的安全性和可读性,防止意外修改。
- 可应用于变量、指针、成员函数等多种上下文。
static
:
- 用于限制变量或函数的作用范围,使其只在声明它的文件或类内部有效。
- 允许定义持久存在的局部变量和共享的类成员。
- 提供了一种管理状态和资源的有效方式,特别是在面向对象编程中。
C++中的重载、重写(覆盖)和隐藏的区别
1. 重载(Overloading)
重载是指在同一作用域内定义多个具有相同名称但参数列表不同的函数。编译器通过参数列表(包括参数的数量、类型和顺序)来区分这些函数。
特点:
- 函数名相同,但参数列表不同。
- 返回值类型可以相同也可以不同,但不能仅依靠返回值类型来区分重载函数。
- 通常用于提供同一操作的不同实现方式。
示例:
#include <iostream>void print(int x) {std::cout << "Integer: " << x << std::endl;
}void print(double x) {std::cout << "Double: " << x << std::endl;
}void print(const char* str) {std::cout << "String: " << str << std::endl;
}int main() {print(42); // 调用 void print(int)print(3.14); // 调用 void print(double)print("Hello"); // 调用 void print(const char*)
}
2. 重写(覆盖,Overriding)
重写发生在派生类中,重新定义基类中的虚函数。重写的目的是实现多态性,使得通过基类指针或引用调用时能够根据实际对象的类型调用相应的函数。
特点:
基类和派生类中函数名、参数列表和返回类型必须完全相同。
基类中的函数必须声明为 virtual,或者使用 override 关键字明确表示重写。
通过基类指针或引用调用时,会根据实际对象类型调用相应的函数。
示例:
#include <iostream>class Base {
public:virtual void show() const {std::cout << "Base class show function" << std::endl;}
};class Derived : public Base {
public:void show() const override { // 使用 override 明确表示重写std::cout << "Derived class show function" << std::endl;}
};int main() {Base base;Derived derived;Base* ptr = &derived;ptr->show(); // 输出 "Derived class show function"
}
3. 隐藏(Hiding)
隐藏是指派生类中的成员函数或变量与基类中的同名成员函数或变量具有相同的名称,但参数列表不同或没有 virtual 关键字,导致派生类中的成员隐藏了基类中的成员。
特点:
- 派生类中的成员函数或变量与基类中的同名成员函数或变量具有相同的名称。
- 如果派生类中的函数参数列表不同,则不会构成重载,而是直接隐藏基类中的同名函数。
- 如果派生类中的函数参数列表相同但没有 virtual关键字,则也会隐藏基类中的同名函数。
示例:
#include <iostream>class Base {
public:void show() const {std::cout << "Base class show function" << std::endl;}void display(int x) const {std::cout << "Base class display function with int: " << x << std::endl;}
};class Derived : public Base {
public:void show() const { // 隐藏基类中的 show 函数std::cout << "Derived class show function" << std::endl;}void display(double x) const { // 隐藏基类中的 display 函数std::cout << "Derived class display function with double: " << x << std::endl;}
};int main() {Derived d;d.show(); // 输出 "Derived class show function"d.display(3.14); // 输出 "Derived class display function with double: 3.14"// 使用基类指针调用显示隐藏的行为Base* basePtr = &d;basePtr->show(); // 输出 "Base class show function"basePtr->display(5); // 输出 "Base class display function with int: 5"
}
实际应用中的注意事项
重载:
- 主要用于提供同一操作的不同实现方式。
- 通过不同的参数列表来区分函数。
重写:
- 实现多态性,确保派生类能够提供特定于自身的实现。
- 必须使用 virtual 或 override 关键字。
隐藏:
- 需要小心处理,避免意外隐藏基类中的成员。
- 可以通过显式调用基类成员来避免隐藏问题。
浅拷贝(Shallow Copy)
浅拷贝是指简单地将一个对象的数据成员逐个复制到另一个对象中。如果对象中有指针成员变量,浅拷贝只会复制指针的值(即地址),而不会为指针所指向的内容分配新的内存空间。因此,两个对象中的指针会指向同一块内存区域。
特点:
- 只复制对象的数据成员,不复制指针所指向的实际数据。
- 如果对象中有指针成员变量,两个对象中的指针会指向同一块内存区域。
- 容易导致悬空指针和内存泄漏问题。
深拷贝(Deep Copy)
深拷贝是指不仅复制对象的数据成员,还会为指针所指向的实际数据分配新的内存空间,并将这些数据复制到新的内存区域。这样,两个对象中的指针会指向不同的内存区域。
特点:
- 不仅复制对象的数据成员,还为指针所指向的实际数据分配新的内存空间。
- 避免了浅拷贝带来的悬空指针和内存泄漏问题。
- 更安全,但可能会增加内存使用量。
类成员初始化方式 | 构造函数的执行顺序 | 为什么用成员初始化列表会快一些
类成员初始化方式
1. 直接初始化:
在声明时直接初始化。
class MyClass {
public:int x = 10; // 直接初始化MyClass() {}
};
2. 成员初始化列表:
在构造函数的参数列表后使用冒号和初始化列表来初始化成员变量。
class MyClass {
public:int x;MyClass() : x(10) {} // 使用成员初始化列表
};
3. 构造函数体内初始化:
在构造函数体内通过赋值操作进行初始化。
class MyClass {
public:int x;MyClass() {x = 10; // 构造函数体内初始化}
};
构造函数的执行顺序
C++中构造函数的执行顺序遵循以下规则:
1. 基类构造函数:
如果当前类是从其他类派生的,则首先调用基类的构造函数。如果存在多个基类,则按照它们在继承列表中的顺序依次调用。
2. 成员变量构造函数:
在进入构造函数体之前,先调用所有成员变量的构造函数。成员变量按照它们在类定义中的声明顺序进行初始化,而不是按照成员初始化列表中的顺序。
3. 构造函数体:
最后执行构造函数体内的代码。
示例:
cpp
深色版本
class Base {
public:Base() { std::cout << "Base constructor" << std::endl; }
};class Derived : public Base {
public:int x;int y;Derived() : x(10), y(20) { // 成员初始化列表std::cout << "Derived constructor" << std::endl;}
};
在这个示例中,构造函数的执行顺序是:
- Base 的构造函数
- x 和 y 的构造函数(按声明顺序,即先 x 后 y)
- Derived 的构造函数体
为什么使用成员初始化列表会快一些
使用成员初始化列表通常比在构造函数体内初始化更快,主要原因如下:
避免默认构造和赋值操作:
- 如果在构造函数体内初始化成员变量,编译器会首先调用该成员变量的默认构造函数来创建它,然后在构造函数体内再对其进行赋值操作。这相当于进行了两次操作:一次默认构造,一次赋值。
- 而使用成员初始化列表可以直接初始化成员变量,跳过默认构造步骤,从而减少一次不必要的构造和赋值操作。
1. 支持常量和引用成员的初始化:
- 常量成员和引用成员必须在声明时或通过成员初始化列表进行初始化,因为它们不能在构造函数体内进行赋值操作。
class MyClass {
public:const int x;int& y;MyClass(int value) : x(value), y(z) {} // 必须使用成员初始化列表
private:int z = 0;
};
2. 支持基类和成员变量的特定初始化:
- 成员初始化列表可以用于指定基类和成员变量的具体初始化方式,这对于某些需要复杂初始化逻辑的情况非常有用。
class Derived : public Base {
public:Derived() : Base(), member1(10), member2(20) {} // 指定基类和成员变量的初始化
};
3. 支持基类和成员变量的特定初始化:
- 成员初始化列表可以用于指定基类和成员变量的具体初始化方式,这对于某些需要复杂初始化逻辑的情况非常有用。
class Derived : public Base {
public:Derived() : Base(), member1(10), member2(20) {} // 指定基类和成员变量的初始化
};