构造函数
类的成员在实例化的时候里面的成员有初始化的需求
C++定义了构造函数的概念去进行成员的初始化,构造函数专门用于成员的初始化
一、C++常见的构造函数
- 默认构造函数:没有参数的构造函数。如果类没有定义任何构造函数,则编译器会自动生成一个默认构造函数。
- 带参数的构造函数:接受一个或多个参数的构造函数,用于在创建对象时进行初始化。
- 带默认参数的构造函数:带有默认参数值的构造函数,允许在创建对象时省略某些参数。这样的构造函数可以为类的不同实例提供更灵活的创建方式。
- 拷贝构造函数:用于从一个已存在的对象创建一个新对象,创建的新对象会拷贝原对象的值。通常会在对象初始化、函数参数传递、函数返回值返回时调用。
- 移动构造函数:用于将资源(例如动态分配的内存、文件句柄等)从一个对象“移动”到另一个对象,通常用于提高性能和资源利用率。
- 析构函数:用于在对象生命周期结束时进行清理工作,例如释放动态分配的内存、关闭文件等。析构函数没有参数,通常在对象销毁时自动调用。
注意: 成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是按照它们在构造函数的初始化列表或构造函数体内的赋值顺序进行的。因此,在构造函数中初始化成员变量时要注意成员变量的声明顺序。
1.默认构造函数
(1)语法
类名()
{
//构造函数实现
}
注意:
1.如果不提供任何构造函数,编译器会自动生成一个默认构造函数,该构造函数不接受任何参数并执行默认的初始化操作。
如果你显式提供了一个带空参数列表的构造函数,编译器将不再生成默认构造函数。
2.如果类中没有显式初始化成员变量,C++会执行默认初始化。
对于内置类型,它们的值将是未定义的;对于类类型,将调用其默认构造函数进行初始化。
(2)代码示例
// 默认构造函数
Person():name(nullptr),age(0)
{
cout << "这是 默认构造函数" << endl;
}
2.带参构造函数
(1)语法
类名(参数列表)
{
//构造函数实现
}
注意:
两种初始化方式: 初始化列表 构造函数体内初始化
(2)代码示例
// 带参构造函数
Person(const char * name)
{
// 传入的参数不为空
if(name != nullptr)
{
this->name = new char[strlen(name) + 1];
strcpy(this->name,name);
}
else
{
this->name = nullptr;
}
this->age = 0;
cout << "这是带参构造函数" << endl;
}
3.带默认参数的构造函数
(1)语法
类名(参数列表,成员变量名=默认值)
{
//构造函数实现
}
注意:
如果时定义和声明分离的情况,只需在声明时写出默认值。在定义时重复提供默认参数值会引发编译错误。因为在C++中,构造函数的默认参数值只能在声明或定义的其中一个地方指定。
(2)代码示例
// 带默认参数的构造函数
Person(const char * name,int age = 18):name(new char[strlen(name)+1]),age(age)
{
strcpy(this->name,name);
cout << "调用 带默认参数的构造函数" << endl;
}
4.拷贝构造函数
(1)语法
类名(const 类名 & 变量名)
{
// const 拷贝构造的实现
}
类名(类名 & 变量名)
{
// 非 const 拷贝构造的实现
}
注意:
- 由于是一个对象去初始化另外一个对象,因此它的参数就固定了。
- 一个本类引用,一般我们用一个已经存在的对象去初始化另外一个对象的时候,这个已存在的对象里面的内容是不能更改的,因此这个本类的引用一般是 const。
- C++ 对类型要求非常严格,所以支持 const 版本和非 const 版本的这两种拷贝构造函数,非 const 版本表示在拷贝过程中支持修改被拷贝的对象。
- 如果没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。
- 默认的拷贝构造函数通常是非 const 版本的,因为它不会声明参数为 const,允许在拷贝过程中修改传入对象内容。
- 在编写拷贝构造函数时,特别是非 const 版本的拷贝构造函数,应该尽量避免修改传入对象的内容,以遵循面向对象编程的封装原则。
- 关于如何调用:关于调用 const 拷贝构造函数还是非拷贝构造函数是根据参数来决定的,也就是你要拷贝的那个对象,如果待拷贝的对象被 const 修饰,就会调用 const 拷贝构造函数。如果待拷贝的对象没有被 const 修饰,就会调用非 const 拷贝构造函数。
深拷贝与浅拷贝:
- 深拷贝: 当对象中含有指针时,深拷贝为对象分配了新的空间,并将原对象的内容复制到了新的空间中。
- 浅拷贝: 浅拷贝只是复制了对象本身的值,没有额外开辟新的空间。浅拷贝会创建一个新对象,该对象与原对象共享相同的内存空间。因此,无论是原对象还是新对象的改变都会影响到另一个对象,因为它们实际上指向相同的内存空间。这可能会导致潜在的问题,特别是在释放内存时,可能会导致重复释放同一块内存空间,从而导致程序崩溃或不确定的行为。
(2)代码示例
// const 拷贝构造函数
Person(const Person & p):name(new char[strlen(p.name)+1]),age(p.age)
{
strcpy(this->name,p.name);
cout << "调用 const 拷贝构造函数" << endl;
}
5.移动构造函数
6.析构造函数
(1)语法
~类名() // 没有参数,没有返回值
{
// 析构函数的实现,可以根据自己的对象实现,写释放资源的代码,简洁
}
注意:
1.自动调用:
析构函数是在对象生命周期结束时自动调用的,用于执行对象资源的清理工作,如释放动态分配的内存、关闭文件等。当对象超出范围、被删除或程序结束时,析构函数会被调用。
2.资源释放:
在析构函数中,一般会编写释放对象所持有资源的代码,例如删除指针、关闭文件句柄等。确保在析构函数中正确释放所有动态分配的资源,以避免内存泄漏和资源泄漏。
3.默认析构:
如果没有显式提供析构函数,编译器会为自动生成一个默认的析构函数。默认析构函数通常是空的,什么也不做。建议自己显示提供析构函数,并使用析构函数进行资源释放,防止内存泄漏
4.虚析构函数:
如果生成的类是多态的,即它拥有虚函数,那么析构函数通常应该声明为虚函数。这样,当使用基类指针删除派生类对象时,可以确保调用正确的析构函数,防止内存泄漏。
5.不能重载:
析构函数没有参数,不能重载
6.析构顺序:
有一个类 ClassA 包含了另一个类 ClassB 的对象。ClassB 的析构函数会在 ClassA 的析构函数之后被调用。这是因为在 ClassA 对象被销毁之前,它包含的 ClassB 对象必须保持有效。
(2)代码示例
// 析构函数
~Person()
{
// 释放资源
if(this->name != nullptr)
delete []this->name;
cout << "调用 析构函数" << endl;
}
7.运行代码
#include <iostream>
#include <string.h>
using namespace std;
class Person
{
private:
char *name;
int age;
public:
// 默认构造函数
Person():name(nullptr),age(0)
{
cout << "这是 默认构造函数" << endl;
}
// // 带参构造函数
// Person(const char * name)
// {
// // 传入的参数不为空
// if(name != nullptr)
// {
// this->name = new char[strlen(name) + 1];
// strcpy(this->name,name);
// }
// else
// {
// this->name = nullptr;
// }
// this->age = 0;
// cout << "这是带参构造函数" << endl;
// }
// 带默认参数的构造函数
Person(const char * name,int age = 18):name(new char[strlen(name)+1]),age(age)
{
strcpy(this->name,name);
cout << "调用 带默认参数的构造函数" << endl;
}
// const 拷贝构造函数
Person(const Person & p):name(new char[strlen(p.name)+1]),age(p.age)
{
strcpy(this->name,p.name);
cout << "调用 const 拷贝构造函数" << endl;
}
// 非const 拷贝构造函数
Person(Person & p):name(new char[strlen(p.name)+1]),age(p.age)
{
strcpy(this->name,p.name);
cout << "调用 非const 拷贝构造函数" << endl;
}
// 移动构造函数
Person(Person && p)
{
this->name = p.name;
p.name = nullptr;
this->age = p.age;
cout << "调用 移动构造函数" << endl;
}
// 析构函数
~Person()
{
// 释放资源
if(this->name != nullptr)
delete []this->name;
cout << "调用 析构函数" << endl;
}
};
Person get_Person()
{
return Person{"小刘",23};
}
int main()
{
cout << "-----------p1-------------" << endl << endl;
Person p1;
cout << "-----------p2-------------" << endl << endl;
Person p2{"小张,20"};
cout << "-----------p3-------------" << endl << endl;
Person p3{"小李",22};
cout << "-----------p4-------------" << endl << endl;
Person p4{p2};
cout << "-----------p5-------------" << endl << endl;
Person p5{p3};
cout << "-----------p6-------------" << endl << endl;
Person p6 = get_Person();
return 0;
}
8.执行结果
二、引用
C++认为使用指针是有风险的 ----- eg:野指针
因此:C++对其进行了改进 ---- 引用
1.什么是引用
引用是一个别名,它为已存在的变量或对象提供了另一个名称。通过引用,可以使用原始变量或对象的名称来访问相同的内存空间。
2.引用条件
引用在定义时必须进行初始化,且一旦初始化之后,它将一直引用相同的对象,无法重新绑定到其他对象。因此,引用必须在声明时初始化,并且无法更改绑定的对象。
3.基本语法
类型 &别名 = 已存在的变量名; // 必须初始化
eg:
int a = 18;
int &b = a;
int &c = b;
======>都是代表一个内存空间,且:&a == &b == &c
4.用途:函数参数传递、函数返回
(1)引用作为参数使用:
通过引用,可以将参数传递给函数,从而避免了参数的拷贝,提高了程序的效率。同时,通过引用传递参数可以实现函数对参数的修改,这样修改后的值会影响到原始变量。
(2)作为返回值返回:
通过返回引用,可以直接返回某个变量或对象,避免了返回对象的拷贝,提高了程序的效率。但是需要注意,不要返回函数中局部变量的引用,因为局部变量在函数执行完毕后会被销毁,返回的引用将会变成悬空引用,导致未定义的行为。
(3)代码示例
#include <iostream>
#include <string>
using namespace std;
class Person
{
private:
string name;
public:
// 构造函数,接受一个字符串参数
Person(const string &name): name(name) {}
// 获取姓名
string getName() const
{
return name;
}
};
// 函数参数传递:修改 Person 对象的姓名
void modifyName(Person &person, const string &newName)
{
person = Person(newName);
}
// 函数返回:创建一个 Person 对象,并返回其引用
Person &createPerson(const string &name)
{
Person *personPtr = new Person(name);
return *personPtr;
}
int main()
{
// 创建一个 Person 对象
Person p("小孔");
cout << "姓名:" << p.getName() << endl;
// 调用函数参数传递,修改姓名
modifyName(p, "小王");
cout << "修改后的姓名:" << p.getName() << endl;
// 调用函数返回,创建并返回一个新的 Person 对象
Person &newPerson = createPerson("小张");
cout << "新创建的姓名:" << newPerson.getName() << endl;
// 释放动态分配的内存
delete &newPerson;
return 0;
}
(4)运行结果
5.引用与指针之间的区别:
(1)引用不存在空引用:因为引用必须要初始化,指针可以是空指针
(2)引用必须初始化,指针可以在任何地方赋值,并且不一定进行初始化
(3)引用比指针的效率高一点,引用没有开辟额外的内存空间,指针是一个指针变量,需要内存空间
(4)引用初始化完成后,就不能引用别人,指针在任意合适的地址,指向其他的空间
6.带引用的构造函数的实现
#include <iostream>
#include <string>
using namespace std;
class MyClass
{
private:
int& ref;
public:
// 构造函数,初始化引用成员
MyClass(int& val) : ref(val) {}
void display()
{
cout << "ref:" << this->ref << endl;
}
};
int main()
{
int x = 5;
MyClass obj(x);
obj.display();
return 0;
}
三、C++对象实例化
1.栈中创建对象
类名 对象名
Person p; // 调用默认构造
类名 对象名{初始值};
Person p{"xiaolong",18};
类名 对象名[对象个数];
Person p[10];
类名 对象名字[元素个数]{构造函数,...};
Person p[3]{Person("小龙",18),Person("小李"),Person()};
2.在堆中创建对象
(1)new 在堆中开辟一个空间,底层是先malloc,再调用构造函数
(2)delete 释放一个堆空间,底层是先调用析构函数,再free
(3)如果是单个指针,用 delete
(4)如果是数组 用delete[] 里面什么都不写
eg:
1.直接使用 new 进行定义
类名 *指针名 = new 类名;
int *p = new int;
Person *p = new Person; // 调用默认构造函数
2.使用 new 进行定义并初始化
类名 *指针名 = new 类名{初始值};
int *p = new int{1024};
Person *p = new Person{"小龙",18};
3.使用 new 定义数组
类名 *指针名 = new 类名[元素个数];
int *p = new int[10];
Person *p = new Person[3];
4.使用 new 定义并初始化数组
类名 *指针名 = new 类名[元素个数]{构造函数(参数列表)...};
int *p = new int[10]{1,2,3,.....};
Person *p = new Person[3]{Person("xiaolong",18),Person("小龙"),Person()};
5.使用delete释放资源
(1)对于普通变量 delete p;
(2)对于数组 delete p[];
3.代码实现
#include <iostream>
#include <string.h>
using namespace std;
class Person
{
string name;
int age;
public:
Person():name("小龙"),age(18)
{
cout << "调用默认构造" << endl;
}
Person(string name,int age):name(name),age(age)
{
cout << "调用带参构造" << endl;
}
};
int main()
{
// ----------在栈中创建对象--------------
// 创建普通对象
Person p;
// 创建普通对象,并初始化
Person p1{"张三",10};
// 创建普通对象数组
Person p2[10];
// 创建普通对象数组,并初始化
Person p3[2]{Person(),Person("李四",20)};
// ----------在堆中创建对象--------------
// 创建指针对象
Person *newp = new Person();
// 创建指针对象,并初始化
Person *newp1 = new Person{"我是new出来的",0};
// 创建指针数组对象
Person *newp2 = new Person[3];
// 创建指针数组对象并初始化
Person *newp3 = new Person[2]{Person("就你是new出来的?",1),Person("我也是new出来的",2)};
delete newp;
delete newp1;
delete []newp2;
delete []newp3;
return 0;
}
注:这种在堆空间中创建指针对象的方式,一定要使用delete在用完之后释放,析构函数不会主动释放这些资源。