目录
C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,通常被称为用户定义的类型。
类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。类中的数据和方法称为类的成员。函数在一个类中被称为类的成员。
类定义是以关键字 class 开头,后跟类的名称。类的主体是包含在一对花括号中。类定义后必须跟着一个分号或一个声明列表。例如,我们使用关键字 class 定义 Box 数据类型,如下所示:
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
private:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
};
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
定义 C++ 对象
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
可以在类的外部使用范围解析运算符 :: 定义该函数
double Box::getVolume(void)
{
return length * breadth * height;
}
在 :: 运算符之前必须使用类名。调用成员函数是在对象上使用点运算符(.),这样它就能操作与该对象相关的数据:
Box myBox; // 创建一个对象
myBox.getVolume(); // 调用该对象的成员函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
无参的构造函数
- #include
-
- using namespace std;
-
- class Line
- {
- public:
- void setLength( double len );
- double getLength( void );
- Line(); // 这是构造函数
-
- private:
- double length;
- };
-
- // 成员函数定义,包括构造函数
- Line::Line(void)
- {
- cout << "Object is being created" << endl;
- }
-
- void Line::setLength( double len )
- {
- length = len;
- }
-
- double Line::getLength( void )
- {
- return length;
- }
- // 程序的主函数
- int main( )
- {
- Line line;
-
- // 设置长度
- line.setLength(6.0);
- cout << "Length of line : " << line.getLength() <
-
- return 0;
- }
带参的构造函数
- #include
-
- using namespace std;
-
- class Line
- {
- public:
- void setLength( double len );
- double getLength( void );
- Line(double len); // 这是构造函数
-
- private:
- double length;
- };
-
- // 成员函数定义,包括构造函数
- Line::Line( double len)
- {
- cout << "Object is being created, length = " << len << endl;
- length = len;
- }
-
- void Line::setLength( double len )
- {
- length = len;
- }
-
- double Line::getLength( void )
- {
- return length;
- }
- // 程序的主函数
- int main( )
- {
- Line line(10.0);
-
- // 获取默认设置的长度
- cout << "Length of line : " << line.getLength() <
- // 再次设置长度
- line.setLength(6.0);
- cout << "Length of line : " << line.getLength() <
-
- return 0;
- }
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Line
{
public:
// 这是构造函数
/*Line(double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}*/
private:
double length;
};
int main()
{
Line line;
// 将Line类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
// 将Line类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
// 无参构造函数,放开后报错:error C2512: “Line”: 没有合适的默认构造函数可用
// 获取默认设置的长度
return 0;
}
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(构成函数重载,但是调用存在歧义) 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
使用初始化列表来初始化字段
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。
Line::Line( double len): length(len) {
cout << "Object is being created, length = " << len << endl;
}
多个字段需要初始化
C::C( double a, double b, double c): X(a), Y(b), Z(c) { .... }
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A {
public:
A(int a) :_a1(a) ,_a2(_a1)
{}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);//先初始a2,再初始化a1
}
注意
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)
3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout<<_a1<<" "<<_a2<
}
private:
int _a2;
int _a1;
}
int main()
{
A aa(1);
aa.Print();// _a1=1,_a2为随机值
//初始化顺序由定义类时的声明顺序决定,所以先初始化_a2,由于初始化_a2时_a1还未初始化,所以为随机值
}
析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
当我们用C语言写栈时,要对栈进行初始化和销毁,在写栈的时候我们可能会忘记把栈销毁,如果不对栈进行销毁,可能会造成内存泄漏,这是很危险的,但当我们学会C++的析构函数之后,析构函数就会自动调用,销毁栈,用起来就会很方便。
- typedef int DataType;
- class Stack
- {
- public:
- Stack(size_t capacity = 3)
- {
- _array = (DataType*)malloc(sizeof(DataType) * capacity);
- if (NULL == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
- _capacity = capacity;
- _size = 0;
- }
- void Push(DataType data)
- {
- // CheckCapacity();
- _array[_size] = data;
- _size++;
- }
- // 其他方法...
-
- //栈的析构函数
- ~Stack()
- {
- if (_array)
- {
- free(_array);
- _array = NULL;
- _capacity = 0;
- _size = 0;
- }
- }
- private:
- DataType* _array;
- int _capacity;
- int _size;
- };
- void TestStack()
- {
- Stack s;
- s.Push(1);
- s.Push(2);
- }
编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
- 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;
- }
![](https://1000bd.com/contentImg/2024/04/21/5da098483ba49bfd.png)
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数? main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date 类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁 ,main函数中并没有直接调用Time类的析构函数,而是显式调用编译器为Date类生成的默认析构函数。
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝函数通常用于:
-
通过使用另一个同类型的对象来初始化新创建的对象。
-
复制对象把它作为参数传递给函数。
-
复制对象,并从函数返回这个对象。
拷贝函数的形式:
classname ( const classname &obj) {
// 构造函数的主体
}
写拷贝构造函数最好加上const,防止误用,把传过来的参数修改了。
拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
如果类中没有显式定义拷贝构造函数,则C++编译器会自动生成一个无参的默认拷贝构造函数,在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// Date(const Date d) // 错误写法:编译报错,会引发无穷递归
// 正确写法
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1;
Date d2(d1);
return 0;
}
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time {
public:
Time() {
_hour = 1;
_minute = 1;
_second = 1;
}
Time(const Time& t) {
_hour = t._hour;
_minute = t._minute;
_second = t._second;
cout << "Time::Time(const 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 d1; // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数,但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数 Date d2(d1);
return 0; }
像Date类可以使用默认的拷贝构造函数,因为只有值的拷贝,是浅拷贝。
如果要实现栈的拷贝,栈的拷贝是深拷贝,需要完成深拷贝的构造函数。因为栈的内存是动态开辟出来的,浅拷贝只能拷贝内存的地址,并不能拷贝内存里的值,所以像这种情况不能使用默认的拷贝构造函数,需要自己定义拷贝构造函数。
![](https://1000bd.com/contentImg/2024/04/21/e4a30fca6f4c63bd.png)
st1和st2指向同一块空间,插入删除数据时会被同时修改,造成数据混乱;调用析构函数时,同一块空间会被析构两次,造成程序崩溃。
void Push(const DataType& data) {
// CheckCapacity();
_array[_size] = data;
_size++;
}
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
-
相关阅读:
引用计数法
不安装运行时运行 .NET 程序 - NativeAOT
Python写猜数游戏
.NET Core 使用 System.Threading.Channels消息队列
idea2023和历史版本的下载
vscode debug python launch.json添加args不起作用
听GPT 讲Istio源代码--pilot
趣学python编程 (五、常用IDE环境推荐)
一文理解Cookie、Session
QLineEdit 使用QValidator 限制各种输入
-
原文地址:https://blog.csdn.net/m0_55752775/article/details/127886971