目录
对象的销毁顺序
类的6个默认成员函数
基本概念:一个类中有6种默认成员函数,分别是构造、析构、拷贝构造、赋值重载、移动构造和移动赋值,如果不主动提供某个默认成员函数,编译器会为我们提供默认情况下的该函数(无参)
注意事项:
1、构造函数不等于默认构造函数,默认构造函数是指一个无参或全缺省构造函数,如果用户显示定义了一个无参或全缺省的构造函数,那么该构造函数是默认构造函数(不过是显式的罢了)
2、一个类中只有一个默认构造函数,否则会出现调用歧义(后续解释)
构造函数
功能:在类实例化对象时根据传入的参数初始化对象
注意事项:
1、构造函数的函数名与所在类的类名相同,且不需要返回值(类名是A,构造函数的名字就叫A)
class Date
{
Date() {}//Date类的构造函数
};
2、构造函数可以重载(同时可以有多个不同的构造函数),对象实例化时编译器会自动调用合适的构造函数(不传参就调无参构造函数,传参就调用符合参数类型的构造函数)
#include <iostream>
using namespace std;
class MyClass
{
public:
// 无参构造函数
MyClass()
{
std::cout << "Default constructor called" << std::endl;
}
// 带一个参数的构造函数
MyClass(int x)
{
std::cout << "Constructor with int parameter called, x = " << x << std::endl;
}
// 带两个参数的构造函数
MyClass(int x, double y)
{
std::cout << "Constructor with int and double parameters called, x = " << x << ", y = " << y << std::endl;
}
};
int main()
{
MyClass obj1; // 调用无参构造函数
MyClass obj2(10); // 调用带一个int参数的构造函数
MyClass obj3(10, 3.14); // 调用带int和double参数的构造函数
return 0;
}
3、当类中有无参构造函数时,如果在实例化对象时想要调用该无参构造,不要在对象名后面加括号,否则编译器会将其解释为函数声明(这被称为最令人困惑的解析),而不是对象实例化,虽然可能不会报错但也无法调用无参构造函数
#include <iostream>
using namespace std;
class MyClass
{
public:
// 无参构造函数
MyClass()
{
std::cout << "Default constructor called" << std::endl;
}
};
int main()
{
MyClass obj1(); // 调用无参构造函数
return 0;
}
4、 对象实例化时没有合适的构造函数就会报错(除了下面的还有更离谱的是如果你显式地将某个构造函数标记为 delete(C++11提供的用于禁用某个类型的构造函数)
,任何尝试调用该构造函数的操作都会导致编译器报错)
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass(int x)
{
std::cout << "Parameterized constructor called with x = " << x << std::endl;
}
};
int main() {
MyClass obj1(10); // 正确,调用有参构造函数
MyClass obj2; // 错误,没有无参构造函数,编译器会报错
}
5、C++11规定在声明内置类型的成员变量时可给默认值,其余未添加的内置类型仍为随机值
class MyClass
{
public:
int a = 10; // 默认值(C++11支持)
int b; // 没有默认值,随机值
};
6、无参的和全缺省的构造函数因为函数重载故可以同时存在,但在程序运行时会造成调用歧义
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass()//调用这个也不用传参
{
std::cout << "Default constructor" << std::endl;
}
MyClass(int x = 0)//调用这个不用传参
{
std::cout << "Constructor with default argument x = " << x << std::endl;
}
};
int main() {
MyClass obj; // 编译器不确定调用哪个构造函数
return 0;
}
调用歧义
基本概念:指的是编译器在有函数重载时无法确定调用哪个具体的函数,导致编译错误
1、多个重载版本都匹配参数类型: 如果多个函数重载版本的参数类型都能接受传递的参数(通过隐式类型转换或默认参数),编译器无法确定应该调用哪个函数
#include<iostream>
void func(int x)
{
std::cout << "int version" << std::endl;
}
void func(double x)
{
std::cout << "double version" << std::endl;
}
int main() {
func(3.5f); // 调用时传递了 float 类型
return 0;
}
//func(3.5f) 传递的是 float 类型,但 float 可以隐式转换为 int 或 double。因为两者都能匹配参数,编译器无法决定应该调用哪个函数
2、默认参数与重载结合使用: 如果重载函数包含默认参数,并且与其他重载函数的签名存在重叠,调用时传递的参数不足以区分重载函数,编译器也会无法确定调用哪个版本
#include<iostream>
void func(int x, int y = 10)
{
std::cout << "int version" << std::endl;
}
void func(double x)
{
std::cout << "double version" << std::endl;
}
int main()
{
func(5); // 调用时传递了单个 int 参数
return 0;
}
//func(5) 会导致歧义。func(int, int) 的第一个参数是 int,并且第二个参数有默认值,所以可以匹配单个 int。但 func(double) 也能接受 int,因为 int 可以隐式转换为 double,导致编译器无法选择
初始化类成员变量的行为分析
问题:编译器生成的默认构造参数什么也不干,为什么?
解释:因为C++将数据分为了内置类型和自定义类型,对于编译器生成的默认构造函数,对内置类型不做处理,对自定义类型会去调用其默认构造函数
- 内置类型:语言自身定义的类型(int、char、double、任何类型的指针等)
- 自定义类型:程序员根据自己需求定义的类型(struct、class等)
#include <iostream>
using namespace std;
class A
{
public:
A()//类A的构造函数
{
_a = 0;
cout << "A()" << _a <<endl;
}
private:
int _a;
};
class Date
{
public:
//这里使用编译器提供的默认构造函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Date d1;//构造一个d1对象
d1.Print();
return 0;
}
补充:成员变量的初始化顺序由它们在类中声明的顺序决定,而不是由构造函数初始化列表的顺序决定
缺省值
基本概念:是编程中为函数、方法或构造函数的参数预设的一个值。当调用该函数或构造函数时,如果没有为该参数显式传递值,那么会使用这个预设值作为该参数的值
分类:全缺省 和 半缺省
- 全缺省:构造函数的所有参数都有缺省值
- 半缺省:构造函数的部分参数有缺省值
半缺省的缺省规则:缺省值必须从右向左逐步定义
void example(int a, int b = 5, int c) { /*...*/ } // 错误:c 没有缺省值
1、函数参数的缺省值:如果在函数声明中为某个参数指定了缺省值,那么在调用函数时可以选择不传入该参数,函数将自动使用这个预设的值
void printMessage(std::string message = "Hello, World!")
{
std::cout << message << std::endl;
}
int main()
{
printMessage(); // 输出: Hello, World!
printMessage("Hi there!"); // 输出: Hi there!
}
2、 构造函数的缺省值:类的构造函数可以为参数提供缺省值,这样创建对象时可以选择性传递参数。未传递的参数将使用缺省值
class MyClass
{
public:
MyClass(int a = 10, int b = 20)
{
std::cout << "a = " << a << ", b = " << b << std::endl;
}
};
int main()
{
MyClass obj1; // 输出: a = 10, b = 20
MyClass obj2(100); // 输出: a = 100, b = 20
MyClass obj3(100, 200); // 输出: a = 100, b = 200
}
注意事项:构造函数中的给成员提供的缺省值会覆盖原先为类中成员提供的默认值(类中成员变量生命时给的值),也就是说这两种给默认值的情况可以同时存在,只不过前者在使用时会覆盖后者给的默认值
补充:绝大多数场景下都需要自己实现构造函数,且更推荐用全缺省构造函数
析构函数
功能:释放对象占用的资源,比如动态分配的内存或其他外部资源
特点:
1、函数名与类名一致,函数名前加~(如果类名是 MyClass
,则析构函数为 ~MyClass()
)
2
、析构函数没有参数,也不能被重载,因为每个类只能有一个析构函数(若未显示定义,则编译器自动提供一个什么都不干的默认析构函数)
3、当对象的生命周期结束时,析构函数会自动调用。例如,当局部对象在其作用域结束时或者使用 delete
删除一个通过 new
分配的对象时,析构函数都会被调用
#include <iostream>
using namespace std;
class Date
{
public:
~Date()
{
cout << this << endl;
cout << " ~Date()" << endl;
}
private:
//声明给缺省值
int _year = 1;
int _month = 1;
int _day =1;
};
void func()
{
Date d2;//实例化对象d2
}//func函数结束,调用d2析构函数销毁数据
int main()
{
func();
Date d1;//实例化对象d1
return 0;
}//main函数结束,调用d1的析构函数销毁数据
4、编译器生成的默认析构函数,对内置类型不做处理,对自定义类型会调用它的析构函数
#include <iostream>
using namespace std;
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
注意事项:
1、类对象本身的销毁工作是由编译器完成的,而类对象申请的内存资源是由该类对象的析构函数完成的,类对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
2、如果类对象中有向堆申请内存资源,那么一定要在函数结束时调用析构函数,且该析构函数要保证释放所有类对象向堆申请的内存资源,否则会造成内存泄漏问题
析构顺序
基本概念:在程序结束时,先定义的类成员会被先析构(还是因为开辟时是压栈...)
#include <iostream>
using namespace std;
class Date
{
public:
~Date()
{
cout << this << endl;
cout << " ~Date()" << endl;
}
private:
//声明给缺省值
int _year = 1;
int _month = 1;
int _day = 1;
};
class Stack
{
public:
Stack(int capacity = 4)
{
_array = (int*)malloc(sizeof(int*) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(int data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
cout << this << endl;
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = nullptr;
}
_size = _capacity = 0;
}
private:
int* _array;
int _capacity;
int _size;
};
int main()
{
Date d1;
Stack st1;
return 0;
}
补充:自定义类型的本质还是内置类型
对象的销毁顺序
从左至右:局部变量(后定义的先析构)——>局部的静态——>全局(后定义的先析构)
如果有new申请的对象,需要手动delete删除该对象,因为这不是该类对象中申请的类对象中的析构函数无法释放这一部分资源
当成栈来理解,先进后出
#include <iostream>
class MyClass
{
public:
MyClass(const char* name) : name(name)
{
std::cout << "构造函数: " << name << std::endl;
}
~MyClass()
{
std::cout << "析构函数: " << name << std::endl;
}
private:
const char* name;
};
// 全局对象
MyClass globalObj1("全局对象 1");
MyClass globalObj2("全局对象 2");
void func()
{
// 局部静态对象
static MyClass staticObj("局部静态对象");
// 局部对象
MyClass localObj1("局部对象 1");
MyClass localObj2("局部对象 2");
// 动态分配的对象
MyClass* dynamicObj = new MyClass("动态分配的对象");
// 手动释放动态对象
delete dynamicObj;
std::cout << "~~手动销毁动态分配的对象~~" << std::endl;
}
int main() {
func(); // 调用函数,创建局部和静态局部对象
return 0;
}
~over~