这里写目录标题
- argc argv
- 继承
- 虚继承
- 多态
- override
- 不加override
- overload
- 纯虚函数和抽象类
- 虚析构和纯虚析构
- static和 const
- extern
- self
- 前置 后置
- 默认构造 析构
- 继承
- 构造函数不能是虚函数
- 派⽣类的override虚函数定义必须和⽗类完全⼀致。 有特列
- 何时共享虚函数地址表
- 智能指针
- arr
- malloc void *
- 解释
argc argv
int argc
和 char **argv
是在 C 和 C++ 中常用的参数列表。它们通常用于命令行应用程序,以便从命令行接收输入参数。
argc
表示命令行参数的数目,其中 argv[0]
是应用程序本身的名称,argv[1]
到 argv[argc-1]
是应用程序接收到的其他参数。这些参数可以在运行时使用,为应用程序提供更多的信息。
例如,对于以下命令行:
$ myapp input.txt -o output.txt -v
argc
将是 4,argv[0]
将是 myapp
,argv[1]
将是 input.txt
,argv[2]
将是 -o
,argv[3]
将是 output.txt
。-v
选项没有被存储在 argv
中,但可以在代码中使用 argc
和 argv
来检查其是否已被设置。
在许多应用程序中,argc
和 argv
都被用来指定应用程序的行为。例如,在一个图像处理应用程序中,可以使用 argv
参数指定要处理的文件名和所需的操作。这使得应用程序可以被设计用于处理不同类型的文件,而无需编写多个版本的代码。
总而言之,argc
和 argv
提供了一种方便的方式,使得命令行应用程序能够接收参数并进行相应的操作。
继承
虚继承
用于菱形继承,子类继承两个父类,两个父类有相同的函数·,子类只需要一个
class Animal
{
public:
int m_Age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
animal为虚基类,只需要一份数据,所以继承animal的时候加virtual就行,而不是 sheep和tuo前面
多态
两类
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址
class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
void DoSpeak(Animal & animal)
{
animal.speak();
}
//
//多态满足条件:
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象
void test01()
{
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
}
多态满足条件
有继承关系
子类重写父类中的虚函数
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
多态使用条件
父类指针或引用指向子类对象
override
重写覆盖,用于子类继承父类时
override 是 C++11 中引入的一个关键字,用于显式地声明某个函数是被重载或覆盖了基类中的虚函数。override 关键字的作用是确保派生类中的某个函数与其基类中的虚函数具有相同的函数签名,并且在派生类中该函数被正确地重写或覆盖了。
使用 override 关键字的语法很简单,只需在派生类中重载虚函数时,在函数声明后面加上 override 关键字即可,例如:
class Base {
public:
virtual void foo();
};
class Derived : public Base {
public:
void foo() override;
};
在上面的代码中,虚基类 Base 中定义了一个名为 foo 的虚函数,而在派生类 Derived 中,我们重新定义了 foo 函数并在其后面加上了 override 关键字,以表明该函数是对基类中的虚函数进行的重载。
需要注意的是,如果在派生类中声明的某个函数加上了 override 关键字,但是该函数与其基类中的虚函数的函数签名不匹配,则会导致编译错误。此外,override 关键字只能用于派生类中对基类中的虚函数进行重载,不能用于普通函数或非虚函数的重载。
总之,override 关键字是 C++11 中用于显式声明派生类中某个函数覆盖了基类中虚函数的机制,能够帮助程序员更加清晰地表明函数重载的意图,从而提高代码的可读性和可维护性。
不加override
是的,如果在派生类中重载一个虚函数,即使不加上 override 关键字,编译器仍然会认为这是对基类中的虚函数进行了覆盖(override)。但是,建议在派生类中重载虚函数时加上 override 关键字,这可以明确地表明程序员的意图,更好地帮助防止误用、增强代码的可读性和可维护性。
加上 override 关键字对于发现代码中的错误也很有帮助,比如如果你想重载基类的某个虚函数,但是在函数名或参数列表中书写错误,那么编译器就会报错,提醒你发现错误。如果你没有加上 override 关键字,而不小心将函数名或参数列表写得与基类中的虚函数不匹配,那么编译器可能会忽略这种错误,导致你的程序在运行时出现许多奇怪的问题,难以排查。
所以,虽然在C++11之前没有override关键字也可以重载虚函数,但建议在C++11或以上版本中,在派生类中重载和覆盖基类中的虚函数时使用 override 关键字,这样可以提高代码的规范性,减少代码出错的概率,增加代码的可读性。
overload
是的,C++中的函数重载(overload)是指在同一个作用域内,可以定义多个同名的函数,但它们的参数列表不同。函数重载可以使程序更加灵活,方便地实现不同类型和数量的参数的处理逻辑。
与 override 不同的是,函数重载并没有关键字来显式地标识一个函数是对另一个同名函数进行了重载,而是通过函数名和参数列表的不同来区分不同的函数。例如:
void func(int x);
void func(int x, int y);
void func(double x);
上述代码中,定义了三个同名函数 func,它们的参数列表不同,分别接收一个整数、两个整数和一个 double 类型的参数。这三个函数是属于同一个作用域的,编译器会根据调用时所传入参数的类型和数量来决定调用哪个函数。
需要注意的是,函数重载的参数列表必须不同,否则编译器无法区分它们,会产生二义性错误。例如下面的代码就会报错:
void func(int x);
void func(int y); // 参数与上面的函数重复了,编译器无法区分
总之,函数重载是 C++ 中的一个重要特性,使我们可以用同一函数名处理多种数据类型和参数个数的情况,方便程序设计和开发。和override不同的是,函数重载不需要关键字来显示表明,而是通过函数名和参数列表的不同来区分。
纯虚函数和抽象类
为了多态而使用虚函数,在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
无法实例化对象
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
不管是栈 还是 堆区,都无法实例化对象
Base b;
new Base 都不行
这里用了多态的方式调用子类函数对象
父类指针或引用指向子类对象
Base * base = NULL; 父类的指针
base = new Son; 指向子类的对象 new返回地址
base->func();
delete base;//记得销毁
虚析构和纯虚析构
static和 const
const
和 static
都是 C++ 中用于修饰变量的关键字,它们各自的作用和区别如下:
- const
const
关键字用于指定一个变量为常量,即该变量的值不能被修改。具体来说,分为两种情况:
- const 修饰变量:可以在声明时初始化或者在定义时初始化,但之后不能再改变其值。
- const 修饰指针:可以修改指针指向的地址,但不能通过该指针修改所指向的变量的值。
举个例子:
const int a = 10; // 常量a的值不能被修改
int b = 5;
const int* p = &a; // 指向常量a的指针,可以修改指针p的指向但不能通过p修改a的值
p = &b; // 合法,p指向了变量b
*b = 20; // 不合法,p指向的是一个常量不能被修改
- static
static
关键字表示一个变量是静态的,即该变量在内存中只有一份拷贝,并且其生命周期与程序运行期间相同,而不是与作用域相关联。具体来说,可以应用于以下场景:
- static 修饰局部变量:意味着该变量在第一次函数调用时被初始化,并在函数调用结束后不会被销毁,下次函数调用时仍然存在。
- static 修饰全局变量:表示该变量只能在当前文件中使用,无法在其他文件中访问。
- static 修饰成员变量:表示该变量是属于类的,而不是属于某个对象的,即该变量在所有对象中共享一份拷贝。
举个例子:
void test() {
static int count = 0; // 静态局部变量,只会被初始化一次
count++;
cout << count << endl;
}
static int var = 10; // 静态全局变量,只能在本文件中访问
class Test {
public:
static int count; // 静态成员变量,属于类
};
int Test::count = 0; // 静态成员变量定义和初始化
总之,const
用于指定变量为常量,而 static
用于指定变量为静态变量,其生命周期与程序运行期间相关联,而不是与作用域相关联。两者的作用和使用场景不同,在实际编程中需要根据具体需求进行选择。
extern
extern
是 C++ 中用于指定变量或函数的链接属性的关键字。具体地说,它有以下两种使用情况:
- 声明全局变量
在某个源文件中声明一个全局变量时,如果希望其他源文件也能够访问该变量,可以使用 extern
关键字进行声明,并在另一个文件中定义该变量。例如,某个源文件中声明了下面的全局变量:
// a.cpp
extern int g_var;
则在另一个源文件中也可以访问这个变量,但不需要在这个源文件中重新定义该变量:
// b.cpp
extern int g_var; // 与 a.cpp 中声明的是同一个变量
int main() {
g_var = 10; // 可以修改 g_var 的值
return 0;
}
- 声明全局函数
在某个源文件中声明一个全局函数时,如果希望其他源文件也能够调用该函数,需要使用 extern
关键字进行声明。例如,某个源文件中声明了下面的全局函数:
// a.cpp
extern void foo();
则在另一个源文件中也可以调用这个函数,但需要保证在程序执行时可以找到该函数的实现。通常的做法是将函数的实现放在另一个源文件中,并将这些源文件编译并链接成一个程序。例如,在另一个源文件中调用该函数:
// b.cpp
extern void foo(); // 与 a.cpp 中声明的是同一个函数
int main() {
foo(); // 调用全局函数 foo
return 0;
}
需要注意的是,extern
关键字只是声明一个变量或函数的存在,并不是定义它们。因此,在使用 extern
声明之后,需要在另一个源文件中进行定义才能保证程序正常运行。
总之,extern
关键字用于指定变量或函数的链接属性,在声明全局变量或函数时使用。通过使用 extern
可以跨源文件访问全局变量或调用全局函数,但需要保证在程序执行时可以找到它们的定义。
self
这是 C++ 中重载后缀自增运算符(operator++(int)
)的代码实现。其中 self
是一个占位符类型,表示当前类的类型。例如,如果这段代码是在类 MyClass
中实现的,那么 self
就表示 MyClass
类型。
在这个重载函数中,我们需要保证返回值是一个临时对象,而不是对原始对象的引用或指针。这样可以避免出现意外的副作用,从而保持程序的正确性和安全性。临时对象的创建和销毁都是由编译器自动进行的,因此通常不会对程序性能产生明显的影响。
为了保证返回的临时对象不会被修改,我们需要使用 const
修饰返回类型。这样可以使得返回对象在调用结束后不能被修改,从而避免了潜在的安全隐患。同时,如果忘记在返回类型中加入 const
修饰符,编译器也会发出警告。
回到 self
类型,它只是一个占位符,并没有任何实际意义。我们可以将 self
替换成类名,或者使用 auto
或模板参数来取代它。例如:
class MyClass {
public:
auto operator++(int) const {
auto tmp = *this;
++*this;
return tmp;
}
};
总之,重载自增运算符后缀形式需要返回一个临时对象,为了保证安全性和正确性,我们需要使用 const
修饰返回类型。而 self
则是一个占位符类型,可以替换成其他类型。
前置 后置
https://blog.csdn.net/MakeYouClimax/article/details/125909515
对于重载运算符来说,const
关键字和 auto
关键字的顺序是没有固定要求的。因此,在这个代码中,const
关键字和 auto
关键字的位置可以互换而不会影响程序的正确性。
例如,下面这两种写法都是合法的:
auto const operator++(int) {
auto tmp = *this;
++*this;
return tmp;
}
const auto operator++(int) {
auto tmp = *this;
++*this;
return tmp;
}
这两种写法的效果是相同的:它们都表示这个运算符函数的返回值类型是 const 自动推导(auto)类型的。也就是说,返回值不能被修改,否则编译器会报错。
需要注意的是,关键字 const
和 auto
的顺序可以与其他关键字同时存在,但是由于 const
关键字改变了函数的语义(指明该成员函数不会修改对象的状态),因此通常建议将其放在 auto
关键字之前,以便更清晰地表达函数的意图。
总之,对于重载运算符来说,const
关键字和 auto
关键字的顺序可以互换而不会影响程序的正确性,但是为了更清晰地表达函数的意图,通常建议将 const
关键字放在 auto
关键字之前。
self &operator++() {
node = (linktype)((node).next);
return *this;
}解释该段代码
这段代码定义了一个成员函数 operator++
,用于对链表节点进行前缀自增操作。
具体来讲,该函数首先通过 (node).next
获取当前节点指向下一个节点的指针,然后将其转换成 linktype
类型,并将其赋值给 node
变量。这个过程实质上就是将当前节点移动到下一个节点的位置。
接着,该函数返回当前对象的引用 *this
。由于该函数返回的是引用类型,因此它可以被连续调用,而且会以修改对象的形式进行。
因此,如果我们在链表节点上调用该函数,它将会将当前节点指向下一个节点,并将自身的引用作为返回值,从而可以方便地进行链式操作。
需要注意的是,这里的 self
类型应该是链表节点类本身,因此 self &
表示返回值是一个指向当前对象的引用类型。一个常见的设计模式是,在重载前缀自增操作符时,返回值通常是一个引用类型,以便支持链式语法。同时,函数也不应该带有参数,否则会导致编译错误。
综上,这段代码定义了一个重载前缀自增操作符的成员函数,用于将链表节点移动到下一个节点,并返回当前对象的引用,以便支持链式语法。
默认构造 析构
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
继承
构造函数不能是虚函数
您说得很对。在 C++ 中,构造函数不能是虚函数,这是因为在创建对象时,先调用构造函数初始化对象的成员变量和数据成员,此时对象还没有完全构造好,无法确定其类型。因此,在构造函数中调用虚函数实际上会执行父类中对应的函数,并且多态特性不会生效。
例如,我们定义一个基类 Base
和一个派生类 Derived
,并在其中分别定义一个虚函数 virtual void func()
:
class Base {
public:
Base() { func(); }
virtual void func() { std::cout << "Base::func()" << std::endl; }
};
class Derived : public Base {
public:
virtual void func() { std::cout << "Derived::func()" << std::endl; }
};
在上述代码中,我们在基类 Base
的构造函数中调用了虚函数 func()
。然后,我们创建一个派生类对象 Derived obj
,并输出该对象的 func()
函数。可以看到,实际执行的是基类 Base
中的 func()
函数,而不是派生类 Derived
中的函数。这是因为对象还没有完全构造好,无法确定其类型,所以多态特性被禁用了。
因此,在编写构造函数时,应尽量避免调用虚函数或其他可能导致多态失效的操作,以确保程序的正确性和可读性。
在 C++ 中,子类的构造函数会自动调用其父类的构造函数。这是因为创建派生类对象时,需要先初始化其基类部分,确保基类对象正确地构造出来,然后再构造派生类对象。
对于虚函数,子类可以在其构造函数中调用父类的虚函数,但由于对象还没有完全构造完成,此时多态特性无法生效,实际执行的是父类的虚函数,而非子类重写的虚函数。例如:
class Base {
public:
Base() { func(); }
virtual void func() { std::cout << "Base::func()" << std::endl; }
};
class Derived : public Base {
public:
Derived() {}
virtual void func() { std::cout << "Derived::func()" << std::endl; }
};
int main() {
Derived obj;
obj.func(); // 输出 Derived::func()
return 0;
}
在上述代码中,我们定义了一个基类 Base
和一个派生类 Derived
,并在其中分别定义了虚函数 func()
。在 Base
类的构造函数中,我们调用了虚函数 func()
。接着,我们创建了一个 Derived
类的对象 obj
,并输出该对象的 func()
函数。由于 Derived
类已经重写了 func()
函数,因此实际上应该输出 Derived::func()
,但是由于在父类 Base
的构造函数中调用了虚函数 func()
,此时对象还没有完全构造完成,多态特性无法生效,因此输出结果为 Base::func()
。
因此,在使用虚函数时,我们需要特别注意其调用时机,并尽量避免在构造函数中调用虚函数,以确保程序的正确性和可读性。
我记得是不是编译阶段就构造了,但是他的构造函数中有虚函数,所以没构造完成
派⽣类的override虚函数定义必须和⽗类完全⼀致。 有特列
在 C++ 中,子类重写(override)一个虚函数时,可以使用返回类型协变的特性,即返回派生类指针或引用。
在使用基类指针或引用调用虚函数时,会根据运行时对象的实际类型动态调用相应的虚函数。由于派生类继承了基类的成员函数,因此可以在派生类中重写虚函数,并使用相同的函数名和参数列表,实现多态。但是,对于返回类型,一般情况下必须保持协变性,即返回类型要与基类的虚函数返回类型一致或是其派生类。
例如:
class Base {
public:
virtual Base* func() { return this; }
};
class Derived : public Base {
public:
virtual Derived* func() override { return this; }
};
int main() {
Derived d;
Base* b = &d;
Base* c = b->func(); // 返回一个 Derived* 类型,但是可以转换为 Base*
return 0;
}
在上述代码中,我们定义一个基类 Base
和派生类 Derived
,并在其中分别定义了虚函数 func()
,返回类型分别为 Base*
和 Derived*
。在派生类中重写了基类的虚函数,并将返回类型修改为 Derived*
,以实现返回派生类指针的效果。在主函数中,我们创建一个 Derived
对象 d
,然后将其地址赋值给基类指针 Base* b
。接着,我们调用基类指针 b
的虚函数 func()
,返回一个 Derived*
指针,并且可以将其转换为 Base*
类型。
需要特别注意的是,在使用返回类型协变时,需要保证派生类中的虚函数与基类中的虚函数具有相同的函数名、参数列表以及 const 限定符,并且返回类型必须是基类虚函数返回类型的派生类或其本身。否则可能会导致编译错误或运行时错误。
何时共享虚函数地址表
如果一个派生类继承的第一个基类定义了虚函数地址表,而且该派生类之前没有其他的虚函数,则该派生类将共享基类的虚函数地址表首地址占用的存储单元。这是因为,在继承期间,派生类的虚函数地址表的前几个条目需要与基类的虚函数地址表保持一致,以便正确地实现多态性。
由于派生类共享基类的虚函数地址表首地址占用的存储单元,因此基类和派生类之间具有可靠的动态类型识别能力,可以在运行时正确地调用对象的虚函数。同时,派生类也可以添加自己的虚函数到虚函数地址表中,这些虚函数将被追加到基类的虚函数后面,以保持正确的虚函数调用顺序。
需要注意的是,如果派生类定义了自己的虚函数之前已经继承过其他的基类,则派生类将独立拥有自己的虚函数地址表,不会再共享基类的虚函数地址表首地址占用的存储单元,而是在自己的虚函数地址表中添加自己的虚函数。这是因为,在 C++ 中,多重继承可能导致派生类继承多个具有不同虚函数地址表的基类,因此需要为每个虚函数地址表分配独立的存储空间来确保正确的虚函数调用顺序。
智能指针
在 C++11 中,标准库提供了 shared_ptr 类模板,可以实现自动引用计数、内存安全的智能指针功能。shared_ptr 同时还支持自定义删除器和自定义分配器,可以灵活地适应不同场景。
shared_ptr 的作用是管理动态分配的对象,以避免内存泄漏和悬空指针等问题。它通过维护一个指向所管理对象的指针以及一个引用计数来实现这个功能,当所有共享该对象的 shared_ptr 对象都被析构时,所管理的对象会被自动释放。
shared_ptr 实现简单,使用方便,可以方便的进行对象的传递和共享,能够大大简化动态内存分配和管理的复杂性,被广泛应用于 C++ 语言中。
这个描
述有点模糊,不过我理解的是你需要实现一个引用计数智能指针,可以对任意类型的对象进行管理,并且需要重载 operator* 和 operator-> 操作符,使得可以像使用原始指针一样访问所管理的对象。
一个通用的引用计数智能指针的实现可以参考如下代码:
template<typename T>
class RefCountPtr {
public:
RefCountPtr() : ptr(nullptr), ref_count(new size_t(1)) {}
RefCountPtr(T* p) : ptr(p), ref_count(new size_t(1)) {}
RefCountPtr(const RefCountPtr& r) : ptr(r.ptr), ref_count(r.ref_count) {
++(*ref_count);
}
RefCountPtr& operator=(const RefCountPtr& r) {
if (this != &r) {
this->~RefCountPtr();
ptr = r.ptr;
ref_count = r.ref_count;
++(*ref_count);
}
return *this;
}
~RefCountPtr() {
if (--(*ref_count) == 0) {
delete ptr;
delete ref_count;
}
}
T& operator*() const {
return *ptr;
}
T* operator->() const {
return ptr;
}
private:
T* ptr;
size_t* ref_count;
};
这个类中,ptr 是所要管理的对象指针,ref_count 是对象的引用计数。当对象被多个 RefCountPtr 所持有时,它的引用计数会相应地增加;当最后一个 RefCountPtr 被销毁时,引用计数会降为 0,此时对象所占用的内存会被自动释放。
operator* 和 operator-> 操作符都是对所管理的对象进行访问的方式,因此需要返回指针所指向的对象本身。在这个实现中,它们都被定义为 const 成员函数,在函数体内直接返回 ptr 所指向的对象即可。
你可以利用 shared_ptr 的构造函数和重载赋值运算符实现这个需求。在构造、赋值时对引用计数进行相应的操作,代码示例如下:
template<typename T>
class RefCountPtr {
public:
RefCountPtr() : ptr(nullptr), ref_count(new size_t(1)) {}
RefCountPtr(T* p) : ptr(p), ref_count(new size_t(1)) {}
RefCountPtr(const RefCountPtr& r) : ptr(r.ptr), ref_count(r.ref_count) {
++(*ref_count);
}
RefCountPtr& operator=(const RefCountPtr& r) {
if (this != &r) {
this->~RefCountPtr();
ptr = r.ptr;
ref_count = r.ref_count;
++(*ref_count);
}
return *this;
}
~RefCountPtr() {
if (--(*ref_count) == 0) {
delete ptr;
delete ref_count;
}
}
T& operator*() const {
return *ptr;
}
T* operator->() const {
return ptr;
}
void reset(T* p) {
RefCountPtr tmp(p);
swap(tmp);
}
void swap(RefCountPtr& r) {
std::swap(ptr, r.ptr);
std::swap(ref_count, r.ref_count);
}
private:
T* ptr;
size_t* ref_count;
};
template<typename T>
void swap(RefCountPtr<T>& lhs, RefCountPtr<T>& rhs) {
lhs.swap(rhs);
}
template<typename T, typename... Args>
RefCountPtr<T> make_ref_counted(Args&&... args) {
return RefCountPtr<T>(new T(std::forward<Args>(args)...));
}
template<typename T>
RefCountPtr<T> make_ref_counted(T&& obj) {
return RefCountPtr<T>(new T(std::forward<T>(obj)));
}
template<typename T>
RefCountPtr<T> make_ref_counted() {
return RefCountPtr<T>(new T);
}
template<typename T>
RefCountPtr<T> make_ref_counted(const RefCountPtr<T>& other) {
return other;
}
template<typename T>
RefCountPtr<T> make_ref_counted(RefCountPtr<T>&& other) {
return std::move(other);
}
这里的构造函数、析构函数、operator* 和 operator-> 都与前面的实现相同,不再赘述。reset 函数接受一个指针,并用一个临时的 RefCountPtr 对象替换当前的对象,以达到重新设置所管理的对象指针的目的。
swap 函数用于交换两个 RefCountPtr 对象持有的资源,由于资源才是真正持有内存的对象,因此除了交换所管理的指针之外,还需要交换引用计数的值。
make_ref_counted 由 C++20 的 std::make_shared 模板函数改编而来,用于方便地创建 RefCountPtr 对象。它包括了几种形式的重载,可以根据不同的要求创建不同的对象。在这里,我们还为 make_ref_counted 提供了移动语义版本和复制语义版本,以兼容不同的使用场景。
如果多个线程同时对一个 shared_ptr 进行循环执行两边 swap 操作,那么最终它所引用的对象的值应该不变。这是因为 swap 操作只交换了两个 shared_ptr 对象内存中的指针和引用计数,而并没有对所指向的内存进行修改。
然而,需要注意的一点是,在多线程环境下对同一 shared_ptr 进行操作,存在着竞态条件。特别地,在一个线程对该 shared_ptr 调用 swap 函数而另一个线程在同时调用其他函数(如 reset、析构函数等)时,就可能会造成未定义行为(如 double free 等问题),从而导致程序出错。
为了避免这种情况的发生,可以采用加锁的方法来保证共享资源的安全访问。例如使用 std::mutex 来保护 shared_ptr 在多线程环境下的访问,或者使用 C++ 中的原子操作,以确保资源的互斥访问。
arr
sizeof(数组名)
这一表达式返回的是数组对象的总字节数,也就是数组中所有元素所占用的内存空间大小。
需要注意的是,sizeof
操作符不仅可以用于数组类型,还可以用于其他类型,如结构体、指针等等。在使用 sizeof
操作符时,我们通常需要将其作为一个表达式的一部分来使用,而非一个函数调用。
例如,对于一个整型数组 arr
,可以使用 sizeof(arr)
来获取数组的字节大小,如下所示:
int arr[5] = {1, 2, 3, 4, 5};
std::cout << "size of arr: " << sizeof(arr) << std::endl;
这段程序会输出 size of arr: 20
,表示 arr
数组所占用的内存空间大小为 20 个字节(假设 int 类型占用 4 个字节)。
需要注意的是,sizeof
操作符返回的是一个编译时期的常量值,因此它可以用于定义数组的大小或作为分配内存的参数等场合。同时,在对数组进行 sizeof 操作时不需要加括号,因为数组名本身已经是一个地址。
另外,在使用 sizeof
操作符时需要特别注意结构体内存对齐等问题,以避免获取到错误的大小结果。
是的,数组名在大多数情况下被解释为指向数组首元素的指针。因此,在许多场景中,我们可以将数组名视为指向数组中第一个元素的指针。
例如,在函数参数声明中,数组的声明通常会写成类似于以下形式:
void foo(int arr[]);
这里的 arr[]
其实就是一个指向整型数组首元素的指针。在函数中,可以通过下标或指针运算来访问数组中的元素。
同样地,在使用指针时,对指针名进行解引用操作(即 *ptr
)也等同于取指向的内存地址处的值。因此,指针和数组名经常在 C/C++ 语言中相互转换,并且它们在指针算术运算、数组下标访问以及函数调用时都有着类似的用法。
需要注意的是,在某些场合下数组名的特殊性可能会导致一些问题,比如在使用 sizeof
操作符时需要注意数组名与指向数组的指针的大小差异,在函数参数传递中需要特别注意数组的长度信息等。因此,在实际编程中需要仔细区分数组名和指针,并掌握它们各自的特点和使用方式。
malloc void *
在使用 malloc
函数动态分配内存时,它会返回一个指向 void 类型的指针,即 void *
。这是因为 malloc
函数并不会预先知道需要分配内存的类型,而是在运行时根据传入的字节数来进行内存分配。因此,malloc
函数返回的指针不指向任何具体的类型,而是一片未初始化的内存区域。
由于 void *
指针可以被隐式转换为任意类型的指针,因此我们可以将其赋值给指向任意类型的指针,如下所示:
int *p1 = (int*) malloc(100);
char *p2 = (char*) malloc(200);
在上面的例子中,将 malloc
函数返回的 void *
指针强制转换为了指向 int
类型和 char
类型的指针,并且分别分配了 100 个字节和 200 个字节的内存空间。需要注意的是,在进行指针类型转换时处于安全考虑需要使用显式强制类型转换。
另外,需要特别注意的是,在使用 malloc
函数分配内存时需要特别小心内存泄漏、越界访问等问题。为了避免这些问题,我们通常会使用 free
函数手动释放内存空间。
解释
void Test4(void)
{
char *str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL) {
strcpy(str, "world");
cout << str << endl;
}
}
解释
这段代码首先在堆上分配了一个大小为 100 字节的内存空间,并将其地址赋值给指针变量 str
,然后使用 strcpy()
函数将字符串 "hello"
复制到该内存空间中。
接着调用 free()
函数释放该内存空间并将指针变量 str
的值赋为 NULL。此时,该内存空间已经被释放,str
不再指向有效的内存地址,因此对指针变量 str
进行访问操作是非法的。
然而在后面的代码中,却没有判断 str
是否为 NULL 就调用了 strcpy()
函数向它所指向的内存空间中复制字符串 "world"
,这会导致程序发生未定义的行为(undefined behavior),可能会导致程序崩溃、数据损坏等问题。
因此,在使用完 malloc()
分配的堆内存后,应该及时调用 free()
函数释放内存,并将指针变量赋为 NULL,避免出现悬挂指针的情况。同时,在释放了内存后,就不能再次访问该指针指向的内存空间,否则将会导致程序运行错误。