1:构造函数
1:为什么使用构造函数
由于类的封装性,一般来说,数据成员是不能被外界访问的,所以对象的数据成员的初始化工作就给共有函数来完成了。如果定义了构造函数,那么只要对象一建立,就可以自动调用构造函数,来完成对象的初始化。
2:构造函数的特点
1:函数名和类名相同
2:没有返回类型
3:对象创建时自动调用
4:可以带参数
5:可以重载
3:构造函数的种类
构造函数只有两种情况,带参数的和不带参数的构造函数。
class student {public:student()//构造函数不带参数{cout << "请输入学号,姓名,成绩" << endl;cin >> no>>name>>score;}student(int n,const char na[],double s)//构造函数带参数{no = n;strcpy(name,na);score = s;}void display(){cout << "学号:" << no << endl;cout << "姓名:" << name << endl;cout << "成绩:" << score << endl;}
private:int no;char name[20];double score;
};
如果想要区分上面的构造函数是如何使用的我们可以在主函数里面创造两个对象来观察一下。
int main()
{student s1(10,"de",88.8);student s2;s1.display();s2.display;return 0;
}
注意s2这个对象创建使用第一个构造函数,不带参数,不能带括号,如果带上括号,会被编译器理解为函数声明。
其中带参数的构造函数也可以使用另外一种方法来对数据初始化,那就是初始化列表
格式如下
类名(参数): 数据成员1(初始化值),数据成员2(初始化值)....
{函数体};
如代码所示(函数体里面无内容)
class Data {
public:Data(int y, int m, int d) :year(y), month(m), day(d)//初始化列表{}void display(){cout << year << "-" << month << "-" << day << endl;}
private:int year, month, day;
};
2:析构函数(主要释放有new动态开辟的空间)
1:析构函数的特点
1:析构函数名是类名在前面加上~取反运算符
2:析构函数没有返回值,没有参数,不能重载
3:当对象生命周期结束后自动调用,主要目的是释放对象所占内存
2:默认析构函数
每一个类都必须要有一个析构函数,如果一个类没有析构函数,系统会自动生成一个公有的析构函数,即默认析构函数,它的定义格式如下
~类名()
{ }
默认析构函数无法回收由new分配的空间,如果对象的成员有使用new分配空间,就必须自己构造析构函数,显示使用delete来释放内存。
代码示例如下
class Array {
public:Array(int);void sort();void show();~Array();
private:int* a;int n;
};
Array::Array(int nn)
{int i;n = nn;a = new int[n];cout << "请输入" << n << "个整数" << endl;for (int i = 0; i < n; i++){cin >> a[i];}
}
void Array::sort()
{for (int i = 0; i < n; i++){for (int j = 0; j < n - i - 1; j++){if (a[j] > a[j + 1]){a[j] ^= a[j + 1];a[j + 1] ^= a[j];a[j] ^= a[j + 1];}}}
}
void Array::show()
{for (int i = 0; i < n; i++){cout << a[i] << " ";}cout << endl;
}
Array::~Array()
{delete[] a;//释放数组内存
}int main()
{Array mya(10);cout << "排序前" << endl;mya.show();cout << "排序后" << endl;mya.sort();mya.show();return 0;
}
我们定义一个构造函数来分配空间,sort函数进行排序,show函数进行打印,最后一个析构函数释放构造函数分配的空间。
3:复制构造函数
我们复制一个整型很简单,但是我们复制类这个变量也简单吗?
1:类对象的复制
class Data {
public:Data(int y, int m, int d) :year(y), month(m), day(d){}Data(const Data &d)//复制函数{year = d.year;month = d.month;day = d.day;}void display(){cout << year << "-" << month << "-" << day << endl;}
private:int year, month, day;
};int main()
{Data d1(2024, 12, 29);d1.display();//Data d2(d1);Data d2=d1;d2.display();return 0;
}
上面主函数里面两种d2写法都可以
复制构造函数的格式如下
类名(const 类名 & 引用对象名)
{复制函数体}
2:复制构造函数注意事项
1:复制构造函数也是一种构造函数,因此函数名和类名相同,并且没有返回类型
2:只有一个参数,它必须是本类类型的一个引用但并不限制为const,一般普通地会加上const修饰,让参数无法改变,以免在调用此函数时被不小心改变
3:是通过参数传进来的对象来初始化另一个对象,简单来说就是用一个对象初始化另一个对象。
3:复制构造函数的调用时机
1:对象通过另一个对象进行初始化
2:对象以值传递的方式传入函数参数
3:对象以值传递的方式从函数返回
class Data {
public:Data(int y, int m, int d) :year(y), month(m), day(d)//初始化列表{}Data(const Data &d){year = d.year;month = d.month;day = d.day;}void display(){cout << year << "-" << month << "-" << day << endl;}
private:int year, month, day;
};
Data fun()
{Data tmp(0, 0, 0);cout << "coping" << endl;return tmp;
}
我们定义一个返回类型为Data的fun函数返回一个tmp变量。
4:深复制和浅复制
在默认复制构造函数里面,编译器会自动生成代码,将老对象的成员值一一赋值给新对象成员。这是浅复制,这种复制是默认复制方法,也就是将对象里面的数据成员进行简单的赋值。大多数情况下浅复制是够用的,但是当成员函数里面出现动态成员,浅复制就不够用了。例如下面代码
#include<iostream>
using namespace std;
class student {
public:student( int n, const char* na, int s){no = n;name = new char[strlen(na) + 1];strcpy(name, na);score = s;}~student(){if (name != NULL)delete[]name;}
private:int no;char* name;int score;
};int main()
{student s1(1, "weiren", 99);student s2(s1);return 0;
}
当我们运行代码的时候,报错了,这是因为当我构造s2的时候,因为没有定义拷贝构造函数,只是将对象成员赋值,也就导致s2的成员name 和s1的成员name指向了同一块空间,而在析构函数销毁这俩个对象时,将同一块空间销毁了两次,所有导致报错解决方式就是,使用深复制。
如下更改实例
#include<iostream>
using namespace std;
class student {
public:student( int n, const char* na, int s){no = n;name = new char[strlen(na) + 1];strcpy(name, na);score = s;}student(const student& s){no = s.no;score = s.score;name = new char[strlen(s.name) + 1];strcpy(name, s.name);}~student(){if (name != NULL)delete[]name;}
private:int no;char* name;int score;
};int main()
{student s1(1, "weiren", 99);student s2(s1);return 0;
}
4:对象指针,对象引用和对象数组
指针和引用没什么好讲的,因为也就是把类型改成了类这个变量。
1:对象指针
其实和struct定义的指针一样,格式就是
类名 * 对象指针名 = 初值;
2:对象引用
格式
类名 & 对象引用名 = 对象名;
3:对象数组(重点)
格式
类名 数组名[整型常量表达式];
如下我定义一个对象数组
class student {
public:student(int n,const char na[],double s){no = n;strcpy(name, na);score = s;}student(){cout << "请输入学号,姓名,成绩" << endl;cin >> no >> name >> score;}void set(int n,const char na[],double s){no = n;strcpy(name, na);score = s;}void display(){cout << "学号" << no << endl;cout << "姓名" << name << endl;cout << "成绩" << score << endl;}private:int no;char name[20];double score;
};int main()
{student* pp[80];for (int i = 0; i < 80; i++){pp[i]=new student(i,"weiren",i+1);}for (int i = 0; i < 80; i++){pp[i]->display();delete pp[i];}
}
4:对象数组指针
只需要把代码改成这样就行了
int main()
{student* pp[80];student* (*p)[80] = &pp;for (int i = 0; i < 80; i++){(*p)[i]=new student(i,"weiren",i+1);}for (int i = 0; i < 80; i++){(*p)[i]->display();delete pp[i];}
return 0;
}
p时对象数组的指针。
5:常对象和常成员
1:常成员函数
使用const说明的成员函数我没成为常成员函数
定义格式如下
返回类型 成员函数名(参数列表) const
{函数体}
说明
1:const放在函数后面,他是函数类型的一部分,在说明函数和定义函数时都要有const
2:常对象只能调用常成员函数
3:设计常函数的好处是,让使用者知道这个函数不会改变对象成员的值
4:有无const可以进行函数重载
观察下面const的函数重载
class point {
private:int x, y;
public:point(int xx, int yy) :x(xx), y(yy) {};void show(){cout << x << " " << y << endl;}void show()const{cout << "const:" << x << " " << y << endl;}
};
int main()
{point p1(66, 77);const point p2(88, 99);p1.show();p2.show();
}
p1会调用没有const的函数,p2反之。
2:常数据成员
使用const说明的数据成员成为常数据成员
常数据成员必须初始化,而且不能改变。常数据成员的初始化只能通过构造函数的初始化列表实现。
看下面代码
class point {
private:const int x, y;//常成员
public:point(int xx, int yy) :x(xx), y(yy) {};//只能使用初始化列表void show(){cout << x << " " << y << endl;}void show()const{cout << "const:" << x << " " << y << endl;}
};
int main()
{point p1(66, 77);const point p2(88, 99);p1.show();p2.show();
}
这个时候x和y的值就不会改变了,因为他们是常成员,但是常成员是不会调用const重载的show函数,因为const重载的show函数只会去找定义对象时是否有const,也就是找常对象。
3:指向对象的常指针
格式
类名 *const 指针变量名 =对象地址;
看下列代码
class point {
private:int x, y;
public:point(int xx, int yy) :x(xx), y(yy) {};void move(int xo, int yo){x += xo;y += yo;}void show(){cout << "(" << x << "," << y << ")" << endl;}
};int main()
{point p1(88,99), p2(1, 2);point* ptr1 = &p1;ptr1 = &p2;point* const ptr2 = &p1;p1.move(1,1);ptr2->show();return 0;
}
不用const修饰我们可以让ptr1先指向p1然后指向p2,但是使用const修饰就会让ptr2只能指向最初的p1,如果修改会报错。
4:指向常对象的指针
格式
const 类名 * 指针名;
如下代码
我没定义const point* pp来储存p1的地址成功了,也就是使用指针储存了常对象,常对象的类型是const point所以只能使用const point*来储存地址。以此可看无法使用point类型的指针来储存,const point类型的p2.
5:对象的常引用
class point {
private:int x, y;
public:point(int xx, int yy) :x(xx), y(yy) { };void move(int xo, int yo){x += xo;y += yo;}void show()const{cout << "const(" << x << "," << y << ")" << endl;}
};
void f1(const point& cpr)
{cpr.show();
}void f2(point &rp)
{rp.show();rp.move(4, 5);
}int main()
{point p1(66,77), p2(88,99);const point cp(44,55);point& rp = p1;const point& crp = p2;f1(p1); f1(cp); f1(rp); f1(crp); f2(p1); f2(rp); f2(p2);return 0;
}
解释一下我们看f1函数的形参,是一个const point类型的常引用,实参也就可以是一般对象,常对象,一般引用和常引用。
但是我们再看f2函数的形参是一个point类型的引用,实参不能是常对象或者常引用。但是我们注意看最后的f2函数调用,我们调用了p2,虽然crp是const point的类型是p2的引用,但是p2本质上还是一个point的一般对象。所以f2依然可以接受p2传递的参数。
6:动态创建对象和释放对象
直接看代码吧
class point {
private:int x, y;
public:point() :x(0), y(0) {};point(int xx,int yy):x(xx),y(yy){}void show(){cout << x << "," << y << endl;}
};int main()
{point* points = new point[10];for (int i = 0; i < 10; ++i) {points[i] = point(i, i);}delete [10]points;return 0;
}
我们使用points来接受new开辟的10个point类型的对象空间。
然后使用for循环对其赋值。
最后手动释放掉。
7:对象的生存期(使用调试功能观察)
上程序
#include<iostream>
using namespace std;
class A {char str[20];
public:A(const char s[]) { strcpy(str, s);cout << str << "A->";}~A(){cout << str << "~A->";}
};class B {char str[20];
public:B(const char s[]){strcpy(str, s);cout << str << "B->";}~B(){cout << str << "~B->";}
};
void fun()
{A a("fun");static B b("fun");
}
A a("global");
int main()
{B b("main");fun();fun();return 0;
}
使用逐语句,重点观察static修饰的B和main函数局部变量的B和全局变量A的析构函数调用时机。