往篇内容:
C++学习笔记(一)
一、C++编译阶段※二、入门案例解析
三、命名空间详解
四、C++程序结构
五、函数基础
六、标识符
七、数据类型
补充:二进制相关的概念
sizeof 运算符简介
补充:原码、反码和补码
补充:ASCII码表
八、内存构成
补充:变量
九、指针基础
十、const关键字
十一、枚举类型
十二、 类型转换
十三、define指令
十四、typedef关键字
十五、运算符
十六、 流程控制
十七、数组
十八、指针
十九、函数进阶
二十、变量进阶
二十一、结构体
二十二、联合
二十三、类与对象
目录
一、构造函数
1、类对象的初始化
每一个对象都应该在它建立的时候就有确定的内容,否则就失去对象的意义了。在系统为对象分配内存时,应该同时对有关的数据成员赋以初值。
首先,不可以在类声明中对成员函数初始化。因为类不是一个实体,而是一种抽象类型,并不占存储空间,显然无处容纳数据。
//错误示例
class Time
{
hour=0; //不能再类声明中对数据成员初始化
minute=0;
sec=0;
};
如果一个类中所有的成员都是公用的,则可以再定义对象是对数据成员进行初始化,如
class Time
{
public:
hour;
minute;
sec;
};
Time t1={14,56,30};
注:如果数据成员是私有的,或者类中由private或protected的数据成员,就不能用这种方法初始化
2、用构造函数实现数据的初始化
构造函数的概念:
构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。构造函数的是在对类声明的时候由类的设计者定义的,程序用户只需在定义对象的同时指定数据成员的初值即可。
注意:构造函数的名字必须与类名同名,而不能任意命名,以便编译系统能识别它,并把它作为构造函数处理。它不具有任何类型,不返回任何值。
示例:
#include<iostream>
using namespace std;
class Time{ //声明Time类
public: //以下为公用函数
Time(){ //定义构造和函数成员,函数名与类名相同
hour=0; //利用构造函数对对象中的数据成员赋初值
minute=0;
sec=0;
}
void set_time();//成员函数声明
void show_time();//成员函数声明
private: //以下为私有数据
int hour;
int minute;
int sec;
};
void Time::set_time()//定义成员函数,向数据成员赋新值
{
cin>>hour;
cin>>minute;
cin>>sec;
}
void Time::show_time()//定义成员函数,向数据成员复赋新值
{
cout<<hour<<":"<<minute<<":"<<src<<endl;
}
int main()
{
Time t1; //建立对象t1,同时调用构造函数t1.Time()
t1.set_time(); //对t1的数据成员赋新值
t1.show_time(); //显示t1的数据成员的值
Time t2; //建立对象t2,同时调用构造函数t2.Time();
t2.show_time(); //显示t2的数据成员的值
return 0;
}
说明:
-
什么时候调用构造函数? 答:在建立类对象时会自动调用构造函数。在建立对象时系统为该对象分配存储单元,此时执行构造函数,就是把指定的初值送到有关数据成员的存储单元中。每建立一个对象就调用一次构造函数。
-
构造函数没有返回值。它的作用只是对对象进行初始化。因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要不同点。
-
构造函数不需要用户调用,也不能被用户调用。构造函数在定义对象时由系统自动执行,而且只能执行一次。构造函数一般声明为public。
-
可以用一个类对象初始化另一个类对象,如:
Time t1; //建立对象t1,同时调用构造函数t1.Time() Time t2=t1; //建立对象t2,并用t1初始化t2
-
在构造函数中不仅可以对数据成员赋初值,而且可以包含其他语句,例如cout语句。但一般并不提倡在构造函数中加入与初始化无关的内容,以保证程序的清晰。
-
如果用户在自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。
3、用带参数的构造函数对不同对象初始化
当用户希望对不同的对象赋予不同的初值,就可以采用带参数的构造函数,在调用不同对象的构造函数时,从外面将不同的数据传递给构造函数,以实现不同对象的初始化。
构造函数首部的一般格式为:
构造函数名 {类型1 形参1,类型2 形参2,……}
前面已经说明用户是不能调用构造函数的,因此无法采用常规的调用函数的方法给出实参(如fun(a,b);)。实参是在定义对象时给出的。
定义对象的一般格式为:
类名 对象名{实参1,实参2,……};
在建立对象时把实参的值传递给构造函数相应的形参,把它们作为数据成员的初值。
示例:
//有两个长方柱,其高、宽、长分别为12,25,30;15,30,21。求它们的体积
#include<iostream>
using namespace std;
class Box{ //声明Box类
public:
Box(int,int,int); //声明带参数的构造函数
int volume(); //声明计算体积的函数
private:
int height; //高
int width; //宽
int length; //长
};
Box::Box(int h,int w,int len)//在类外定义带参数的构造函数
{
height=h;
width=w;
length=len;
}
int Box::volume()//定义计算体积的函数
{
return height*width*length;
}
int main()
{
Box box1(12,25,30);.//定义对象
cout<<"The volume of box1 is "<<box1.volume()<<endl;
Box box2(15,30,21);
cout<<"The volume of box2 is "<<box2.volume()<<endl;
return 0;
}
可以看到
-
带参数的构造函数中的形参,其对应的实参是在建立对象时给定的,即在建立对象时指定数据成员的初值。
-
定义不同对象时用的实参是不同的,它反映不同的对象属性。用这种方法可以方地实现对不同对象进行不同的初始化
4、在构造函数中用参数初始化表对数据成员初始化
这种方法不在函数体内对数据成员初始化,而是在函数首部实现。
形式:
类名::构造函数名([参数表])[:成员初始化表]
{
//[ ]里的内容可有可无
[构造函数体]
}
例如,将 Box::Box(int h,int w,int len){height=h;width=w;length=len;}
修改为
Box::Box(int h,int w,int len):height(h),width(w),length(len){}
即在原来函数首部的末尾加一个冒号,然后列出参数的初始化表。
示例:
class Student
{public:
Student(int n,char s,name[]):num(n),sex(s) //定义构造函数
{
strcpy(name.nam); //函数体
}
private:
int num;
char sex;
char name[20];
};
5、可以对构造函数进行重载
定义:
在一个类中可以定义多个构造函数,以便为对象提供不同的初始化方法,供用户选用。这些构造函数具有相同的名字,而参数的个数或参数的类型不相同。
//有两个长方柱,其高、宽、长分别为12,25,30;15,30,21。求它们的体积
#include<iostream>
using namespace std;
class Box
{
public:
Box();//声明一个无参构造函数
//定义一个有数,用参数的初始化表对数据成员初始化
Box(int h,int w,int len):height(h),width(w),length(len){}
int volume();
private:
int height;
int width;
int length;
};
Box::Box()
{
height=10;
width=10;
length=10;
}
int Box::volume()
{
return (height*width*length);
}
int main()
{
Box box1;
cout<<"The volume of box is"<<box1.volume()<<endl;
Box box2(15,30,25);
cout<<"The volume of box is"<<box2.volume()<<endl;
return 0;
}
说明:
-
在建立对象是不必给出实参的构造函数,称为默认构造函数。无参构造函数属于默认构造函数。一个类只能有一个默认构造函数。如果用户未定义构造函数,则系统会自动提供一个默认构造函数,但它的函数体是空的,不起初始化作用。如果用户希望在创建对象时就能使数据成员有初值,就必须自己定义构造函数。
-
如果在建立对象时选用的是无参构造函数,应注意正确书写定义对象的语句。
-
尽管在一个类中可以包含多个构造函数,但是对于每一个对象来说,建立对象时只执行其中一个构造函数,并非每个构造函数都被执行。
6、构造函数可以使用默认参数
构造函数中参数的值即可以通过实参传递,也可以指定为默写默认值,即如果用户不指定实参值,编译系统就使形参的值为默认值。
//有两个长方柱,其高、宽、长分别为12,25,30;15,30,21。求它们的体积
#include<iostream>
using namespace std;
class Box
{
public:
Box(int h=10,int w=10,int len=10);//在声明构造函数Box时指定默认参数
int volume();
private:
int height;
int width;
int length;
};
Box::Box(int h,int w,int len) //在定义Box函数时可以不指定默认参数
{
height=h;
width=w;
length=len;
}
int Box::volume()
{
return height*width*length;
}
int main()
{
Box box1; //没有给定实参
cout<<"The volume of box1 is "<<box1.volume()<<endl;
Box box2(15); //只给定一个实参
cout<<"The volume of box2 is "<<box2.volume()<<endl;
Box box3(15,30); //只给定两个实参
cout<<"The volume of box3 is "<<box3.volume()<<endl;
Box box4(15,30,20); //只给定三个实参
cout<<"The volume of box4 is "<<box4.volume()<<endl;
return 0;
}
说明:
-
应该在什么地方指定构造函数的默认参数? 答:在声明构造函数时指定默认值,而不能只在定义构造函数时指定默认值。因为类定义时放在头文件中的,它是类的对外接口,用户可以看到的,而函数的定义是类实现的细节,用户往往是看不到的。在声明构造函数时指定默认参数值,使用户知道在建立对象时怎么使用默认参数。
-
Box(int h=10,int w=10,int len=10);可以省略形参名,即 Box(int=10,int=10,int=10);
-
如果构造函数的全部参数都指定了默认值,则在定义对象时可以给一个或多个实参,也可以不给出实参。由于不需要实参也可以调用构造函数,因此全部参数都指定了默认值的构造函数也属于默认构造函数。一个类只能有一个默认构造函数,也就是说,可以不使用参数而调用的构造函数,一个类只能有一个,是为了避免调用时的歧义性。
-
在一个类中定义了全部是默认参数的构造函数后,不能再定义重载构造函数。 例如在一个类中有以下构造函数的声明:
Box(int =10,int =10,int =10); //指定全部为默认参数 Box(); //声明无参的构造函数 Box(int,int); //声明有两个参数的构造函数
若有以下定义句:
Box box1; Box box2(15,30);
应该执行哪个构造函数呢?出现了歧义性。但如果构造函数中的参数并非全部为默认值时,就要分析具体情况。如有以下3个原型声明:
Box(); //无参的构造函数 Box(int,int =10,int =10); //有一个参数不是默认值 Box(int,int); //有两个参数的构造函数
若有以下定义对象的语句:
Box box1; //正确,不出现歧义性,调用第1个构造函数 Box box2(15); //调用第2个构造函数 Box box3(15,30); //错误,出现歧义性
7、用构造函数实现初始化方法的归纳
-
无参构造函数(固定初值)
在构造函数体中直接为数据成员赋值,适用于需要给所有对象设置相同初始值的场景。 示例:
class Time {
public:
// 无参构造函数,为所有对象的时、分、秒赋固定初值0
Time() {
hour = 0;
minute = 0;
sec = 0;
}
private:
int hour, minute, sec;
};
特点:创建对象时无需传递参数,所有对象的初始值一致。
-
带参构造函数(动态赋值)
通过构造函数的参数接收外部值,在函数体中为数据成员赋值,实现对象的个性化初始化。 示例:
class Box {
public:
// 带参构造函数,接收外部传入的高、宽、长,为成员变量赋值
Box(int h, int w, int len) {
height = h;
width = w;
length = len;
}
private:
int height, width, length;
};
特点:创建对象时需传递参数,可根据需求灵活初始化不同对象的成员变量。
-
参数初始化表(简洁初始化)
通过参数初始化表直接对数据成员赋值,省去函数体中的赋值语句,使构造函数更精炼。 示例:
class Box {
public:
// 用参数初始化表为成员变量赋值,函数体可为空
Box(int h, int w, int len) : height(h), width(w), length(len) {}
private:
int height, width, length;
};
特点:语法更简洁,初始化逻辑清晰,尤其适合成员变量较多的场景。
4. 带默认参数的构造函数
在声明构造函数时为参数指定默认值,调用时可根据需要省略部分或全部参数,兼顾灵活性与简洁性。 示例:
class Box {
public:
// 为参数指定默认值(h、w、len默认值均为10)
Box(int h=10, int w=10, int len=10) : height(h), width(w), length(len) {}
private:
int height, width, length;
};
特点:
-
可像无参构造函数一样使用(
Box b;,使用默认值); -
也可像带参构造函数一样使用(
Box b(5, 8, 12);,覆盖默认值); -
减少重载函数的定义,代码更简洁。
-
构造函数重载(多场景适配)
在同一个类中定义多个同名构造函数(参数列表不同,包括参数个数或类型),以适配不同的初始化场景。 示例:
class Box {
public:
Box() : height(10), width(10), length(10) {} // 无参构造(默认值)
Box(int h, int w) : height(h), width(w), length(10) {} // 2个参数
Box(int h, int w, int len) : height(h), width(w), length(len) {} // 3个参数
private:
int height, width, length;
};
特点:根据创建对象时提供的参数数量 / 类型,自动匹配对应的构造函数,灵活应对多样化的初始化需求。
通过上述方式,可根据实际场景选择合适的构造函数实现,既能保证对象初始化的准确性,又能提升代码的简洁性和可维护性。
8、复制构造函数
对象的复制机制能用一个已有的对象快速地复制出多个完全相同的对象,一般形式为:
类名 对象2(对象1);
在建立对象是调用一个特殊的构造函数——复制构造函数。
函数形式示例:
//复制析构函数的定义
Box::Box(const Box& b)
{
height=b.height;
width=b.width;
length=b.length;
}
复制构造函数也是构造函数,但是它只有一个形参,这个参数是本类的对象(不能是其他的对象),而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免再调用此函数是因不慎而使实参对象被修改)。此复制构造函数的作用就是将实参对象的各个成员值一一赋值给新的对象中对应的成员。
当复制对象时,如果用户未定义复制构造函数,则编译系统会自动提供一个默认的赋值构造函数,其作用只是简单地复制类中每一个数据成员。
普通构造函数和复制构造函数的区别:
1. 定义与语法
-
普通构造函数:用于创建对象时初始化对象的成员变量。
class MyClass { public: // 普通构造函数 MyClass(int value) { /* ... */ } }; -
复制构造函数:用于创建一个新对象,该对象是已有对象的副本。
class MyClass { public: // 复制构造函数 MyClass(const MyClass& other) { /* ... */ } };
2. 参数类型
-
普通构造函数:接受任意类型的参数(如
int,string等)。 -
复制构造函数:接受同类对象的引用(通常是
const引用,防止修改原对象)。
3. 调用时机
-
普通构造函数:在创建新对象时直接调用。
MyClass obj(10); // 调用普通构造函数
-
复制构造函数:在以下场景调用:
-
对象初始化:用一个已存在的对象初始化新对象。
MyClass obj1(10); MyClass obj2 = obj1; // 调用复制构造函数
-
对象作为参数传递:按值传递对象时。
void func(MyClass obj) { /* ... */ } func(obj1); // 调用复制构造函数创建临时副本 -
函数返回对象:函数返回对象时。
MyClass createObj() { MyClass temp(20); return temp; // 可能调用复制构造函数(C++17 后可能优化) }
-
4. 默认行为
-
普通构造函数:必须显式定义,否则编译器不会自动生成(除非没有其他构造函数)。
-
复制构造函数:如果未显式定义,编译器会生成一个默认复制构造函数,逐个复制对象的所有成员变量(浅拷贝)。
5. 深拷贝 vs 浅拷贝
-
默认复制构造函数:执行浅拷贝,仅复制成员变量的值。若对象包含动态分配的资源(如指针),多个对象可能指向同一块内存,导致析构时重复释放。
-
自定义复制构造函数
:需实现深拷贝,为新对象分配独立的资源。
class MyClass { private: int* data; public: // 自定义复制构造函数(深拷贝) MyClass(const MyClass& other) { data = new int(*other.data); // 分配新内存并复制值 } };
6. 示例对比
class Student {
private:
int id;
string name;
public:
// 普通构造函数
Student(int _id, string _name) : id(_id), name(_name) {}
// 复制构造函数
Student(const Student& other) : id(other.id), name(other.name) {}
};
// 使用示例
Student s1(1, "Alice"); // 普通构造函数
Student s2 = s1; // 复制构造函数
Student s3(s1); // 复制构造函数
总结
| 特性 | 普通构造函数 | 复制构造函数 |
|---|---|---|
| 参数类型 | 任意类型 | 同类对象的引用 |
| 调用时机 | 创建新对象时 | 对象复制场景(初始化、传参等) |
| 默认生成 | 否(除非无其他构造函数) | 是(浅拷贝) |
| 资源管理 | 初始化资源 | 需处理深拷贝(若含动态资源) |
9、转换构造函数
作用:将一个其他类型的数据转换成一个类的对象
转换构造函数只有一个形参,如:Complex(double r){Complex &c;}
其作用是将double类型的参数r转换成Complex类的对象,将r作为复数的实部,虚部为0.用户可以根据需要定义转换构造函数,在函数体中高数编译系统怎样去转换。
在类体中,转换构造函数可有可无,视情况而定
示例:
#include <iostream>
#include <string>
using namespace std;
// 定义一个表示长度的类,单位为米
class Length {
private:
double meters;
public:
// 普通构造函数
Length(double m) : meters(m) {
cout << "普通构造函数被调用,长度: " << meters << " 米" << endl;
}
// 转换构造函数:将厘米转换为米
Length(int cm) : meters(cm / 100.0) {
cout << "转换构造函数被调用,将 " << cm << " 厘米转换为 " << meters << " 米" << endl;
}
// 转换构造函数:将英尺和英寸转换为米
Length(int feet, int inches) : meters((feet * 12 + inches) * 0.0254) {
cout << "转换构造函数被调用,将 " << feet << " 英尺 " << inches
<< " 英寸转换为 " << meters << " 米" << endl;
}
// 获取长度(米)
double getMeters() const {
return meters;
}
// 显示长度信息
void display() const {
cout << "长度: " << meters << " 米" << endl;
}
};
// 接受Length对象的函数
void printLength(const Length& len) {
cout << "打印长度: " << len.getMeters() << " 米" << endl;
}
int main() {
// 1. 使用普通构造函数创建对象
Length l1(2.5);
l1.display();
// 2. 使用转换构造函数:int -> Length(厘米转米)
Length l2 = 175; // 等价于 Length l2(175);
l2.display();
// 3. 隐式类型转换
printLength(200); // 200厘米隐式转换为Length对象
// 4. 使用转换构造函数:英尺和英寸 -> Length
Length l3(5, 9); // 5英尺9英寸
l3.display();
// 5. 直接调用转换构造函数
Length l4 = Length(6, 2); // 6英尺2英寸
l4.display();
return 0;
}
关键说明:
-
转换构造函数定义:
-
Length(int cm):将厘米转换为米 -
Length(int feet, int inches):将英制单位转换为米
-
-
隐式类型转换:
-
printLength(200):整数 200 被隐式转换为 Length 对象 -
可以使用
explicit关键字禁用隐式转换:explicit Length(int cm)
-
-
多种转换方式:
-
直接初始化:
Length l2(175) -
拷贝初始化:
Length l2 = 175 -
函数参数传递:
printLength(200)
-
-
转换构造函数与普通构造函数的区别:
-
转换构造函数是单参数的构造函数(或除第一个参数外其余参数都有默认值)
-
普通构造函数可以有多个必需参数
-
转换构造函数为不同类型之间的交互提供了便利,但过度使用可能导致代码可读性下降,因此建议谨慎使用隐式转换,或使用explicit关键字明确禁止隐式转换。
转换构造函数也是一种构造函数,它遵循构造函数的一般规则。通常把有一个参数的构造函数用作类型转换。转换构造函数只能有一个参数。如果有多个参数,就不是转换构造函数了。原因就是如果有多个参数,究竟是把哪一个参数转换成类对象呢?
归纳(使用构造函数将一个指定的数据转换为类对象的方法):
-
先声明一个类
-
在这个类中定义一个只有一个参数的构造函数,参数的类型是需要转换的类型,在函数体中指定转换方法。
-
在该类的作用域内可以用以下形式进行类型转换: 类名 (指定类型的数据) 就可以将指定类型的数据转换为此类的对象。不仅可以将一个标准类型数据转换成类对象,也可以将另一个类的对象转换成构造函数所在的类对象。 例如:将一个学生类对象转换为教师类对象,要求把某学生的编号、姓名、性别复制到一个教师类对象中,可以在Teacher类中写出下面的转换构造函数: Teacher(Student& s){num=s.num;strcpy(name,s.name);sex=s.sex;} 但要注意:对象s中的num,name,sex必须是共用成员,否则不能被类外使用
二、析构函数
1、析构函数简介
析构函数也是一种特殊的成员函数,它的作用与构造函数相反,它的名字使类名的前面加一个“~”符号。在C++中“~”是取反运算符,从这点也可以想到:析构函数是与构造函数作用相反的函数。
当对象的生命期结束的时候,会自动执行析构函数。具体而言是出现以下几种情况就会执行析构函数。
如果一个函数中定义了一个对象(假设是自动局部变量),当这个函数被调用是,对象应该释放,在对象释放前自动执行析构函数。
静态局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序是才调用static局部对象的析构函数。
如果定义了一个全局的对象,则在程序的流程离开其作用域时(如main函数结束调用或调用exit函数)时,调用该全局对象的析构函数。
如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。
析构函的作用不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,这部分内存可以被程序给配给新对象使用。
程序设计者要事先设计好析构函数,以完成所需的功能,只要对象的生命结束,程序就自动执行析构函数来完成这些工作。
析构函数不返回任何值,没有函数类型,也没有函数参数。由于没有函数参数,因此它不能被重载。一个类可以有多个构造函数,但是只能有一个析构函数。
实际上,析构函数的作用不仅限于释放资源方面,它还可以被用来执行“用户希望在最后一次使用对象之后所执行的任何操作”,例如输出有关信息。这里说的用户是指类的设计者,因此,析构函数是在声明类的时候定义的。也就是说析构函数可以完成类设计者所指定的任何操作。
一般情况下,类的设计者应当在声明类的同时定义析构函数,以指定如何完成“清理”的工作。如果用户没有定义析构函数,C++编译系统胡自动生成一个析构函数,但它只是虚有其表,实际上什么操作也不执行。想让析构函数完成任何工作,都必须在定义的析构函数中指定。
示例:
#include<iostream>
#include<string>
using namespace std;
class Student
{
public:
Student(int n, string nam, char s) // 定义含有参数的构造函数
{
num = n;
name = nam;
sex = s;
cout << "Constructor called. num=" << num << endl;//输出有关信息
}
~Student() //定义析构函数
{
cout << "Destructor called. num=" << num << endl;//输出指定信息
}
void display() //定义成员函数
{
cout << "num: " << num << endl;
cout << "name: " << name << endl;
cout << "sex: " << sex << endl;
}
private:
int num;
string name;
char sex;
};
int main()
{
Student stud1(10010, "Niuma", 'f');
stud1.display();
Student stud2(10011, "Nuli", 'm');
stud2.display();
return 0;
}
2、调用构造函数和析构函数的顺序
一般情况下,调用析构函数的次序正好与构造函数的次序相反:最先被调用的析构函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。
简记:先构造的后析构,后构造的先析构

全局对象:定义在所有函数之外的对象,构造函数在本文件模块所有函数(含 main )执行前调用;若多文件定义全局对象,构造顺序不确定 。程序因 main 执行完或 exit 终止时,调用析构函数。
局部自动对象:在函数内定义的对象,建立对象时调用构造函数;若函数多次调用,每次建立对象都调构造函数 。函数调用结束、对象释放前,先调用析构函数。
静态局部对象:函数内用 static 定义的对象,程序首次调用该函数定义对象时,调一次构造函数;函数调用结束对象不释放,不调析构函数 。仅在 main 结束或 exit 终止程序时,调用析构函数。
注:作为用户可以不关心构造函数的具体写法和在构造函数中如何赋值的细节,只要知道构造函数的原型(有几个形参、形参的类型和顺序)一i怎样使用即可,并不需要C++的编程人员都自己写构造函数。
469

被折叠的 条评论
为什么被折叠?



