day4 cpp:
0.面向对象思想
1.类的定义
访问修饰符
//编程规范
//类名遵循大驼峰原则
class Computer {//成员函数名称遵循小驼峰原则void setBrand(const char* brand) {strcpy(_brand, brand);}void setPrice(double price) {_price = price;}//数据成员的名称前面加上下划线char _brand[20];double _price;
};
void test0(){int num;//pc称为Computer类的对象Computer pc;pc.setBrand("ASUS");pc.setPrice(2000);
}
上述代码是错误的,因为setBrand和setPrice是私有成员,在类外不能被访问
public:共有访问权限,在类外可以通过对象直接访问共有成员(提供给对外的接口)
protected:保护的访问权限,在本类中和派生类中可以访问,在类外不能通过对象直接访问
private:私有访问权限,在本类之外不能访问,比较敏感的数据设为private,类定义中可以访问
注意:类定义中访问修饰符的管理权限是从当前行到下一个访问修饰符或类定义结束;
c++中的struct对c进行了扩展,struct中增加了对成员函数的定义,基本等同于class,默认访问权限是public,c++中的class默认访问权限是private
成员函数的定义
//编程规范
//类名遵循大驼峰原则
class Computer {//类定义中没有显式的写出访问修饰符//默认是私有的访问权限
public://成员函数名称遵循小驼峰原则void setBrand(const char* brand);void setPrice(double price);void print();
private://数据成员的名称前面加上下划线char _brand[20];double _price;
};
//成员函数在类定义之外完成定义
//需要在函数名前面加上类作用于限定
void Computer::setBrand(const char* brand) {strcpy(_brand, brand);
}
void Computer::setPrice(double price) {_price = price;
}
void Computer::print() {cout << "brand" << _brand << endl;cout << "price" << _price << endl;
}
void test0(){int num;//pc称为Computer类的对象Computer pc;pc.setBrand("ASUS");pc.setPrice(2000);
}
方法一:将声明放在.h,定义放在.c中
方法二:将声明和加上inline的定义都放在.h
(若声明和定义分开会导致找不到定义错误,或转为普通函数)
方法三: 将成员函数放在类内部定义(全放在.h中,等同于inline)
2.对象的初始化创建(构造函数)
无参构造是Point pt而不是Point pt()
构造函数的使用规则
1.当类中没有显示定义构造函数时,编译器会自动生成一个默认无参构造函数,但不会初始化数据成员
2.当类中显示提供了构造函数时,编译器就不会再生成默认的构造函数
3.编译器自动生成的默认构造函数是无参的;构造函数也可以接收参数(显示定义有参构造函数),在对象创建时提供更大的自由度
4.如果还希望通过无参构造函数创建对象,则必须进行手动提供一个无参构造函数
5.构造函数可以重载,一个类可以定义出多个构造函数,即创建对象时可以用多种方式对数据成员进行初始化
对象的数据成员初始化
对构造函数的函数体中进行赋值,严格意义上不算初始化(而是算赋值)---int x;x=10;
对于类中数据成员的初始化,推荐使用初始化列表完成-----------------------int x=10;
class Point {
public://初始化中对数据成员进行初始化是严格的初始化Point() :_x(0),_y(0) //int _x=0; int _y=0{}Point(int x):_x(x),_y(x+10){}Point(int x,int y):_x(x),_y(y){}void setX(int x) {_x = x;}void setY(int y) {_y = y;}void print() {cout << "x=" << _x << endl;cout << "y=" << _y << endl;}
private:int _x;int _y;
};
用默认参数有可能出现冲突问题,若声明和定义分开,和普通函数一样建议在声明中写默认值,实现一定在声明之后,可以在定义之前
数据成员的初始化顺序和声明的顺序一致,与初始化列表中的顺序无关,调换了初始化列表中的顺序还会和声明中顺序一致
对象所占空间大小
class A {double _price;int _num;
};
class B {int _price;int _num;
};
int main(void) {cout << sizeof(A) << endl;//16cout << sizeof(B) << endl;//8return 0;
}
对象的大小跟对象内所有数据成员所占内存有关,内存对齐导致上述结果
指针数据成员(const char*-->char*)
class Computer {
public:Computer(const char* brand, double price):_brand(new char[strlen(brand) + 1]()), _price(price)//初始化char* _brand=new char[strlen(brand)+1]();{strcpy(_brand, brand);}
private:char* _brand;double _price;
};
上述代码也可以:
class Computer {
public:Computer(const char* brand, double price):_brand(new char[strlen(brand) + 1]{'h','e','l','l','o','\0'}), _price(price){}
private:char* _brand;double _price;
};
3.对象的销毁(析构函数)
注:析构函数只负责清理对象的数据成员申请的资源(堆空间)---不负责清理数据成员(系统自动完成)
自定义析构函数
class Computer {
public:Computer(const char* brand, double price):_brand(new char[strlen(brand) + 1]()), _price(price)//初始化char* _brand=new char[strlen(brand)+1]();{strcpy(_brand, brand);}~Computer(){if (_brand) {delete[]_brand;_brand = nullptr;//安全回收,否则手动调用析构函数会重复调用}}
private:char* _brand;double _price;
};
手动调用析构函数导致销毁堆空间使_brand=nullptr
这时再调用pc.print()执行到<<后,
输出流运算符对char*指针有默认重载效果,故会报错
若不是char*则不会报错,而是返回地址
因此不建议手动调用析构函数,而是让析构函数在对象销毁时自动调用
析构函数调用时机(**)
对于4:
不管是栈中还是堆中,销毁顺序都按照这个存储顺序销毁,但要具体分析不同位置变量的顺序(如先销毁局部变量,再销毁全局变量)!!!!!!!
4.本类型对象的复制(拷贝构造和赋值运算符函数)
int x=1;
int y=x;
Point ptr(0,1);
Point ptr2=ptr;
拷贝构造函数
class Computer {
public:Computer(const char* brand, double price):_brand(new char[strlen(brand) + 1]()), _price(price)//初始化char* _brand=new char[strlen(brand)+1]();{strcpy(_brand, brand);}Computer(const Computer& ref) //编译器提供的默认拷贝构造:_brand(ref._brand),_price(ref._price){}~Computer(){if (_brand) {delete[]_brand;_brand = nullptr;//安全回收}}
private:char* _brand;double _price;
};
void test0() {Computer ptr1("father", 200);//利用一个已经存在的对象用复制的方式创造出新的对象//调用拷贝构造,用=连接为了跟内置类型保持一致Computer ptr2 = ptr1;// or Computer ptr2(ptr1);
}
浅拷贝(默认拷贝)问题
进行浅拷贝时,只是使pc2指向pc1的内容,当delete pc2时就会销毁这片空间,但pc再次销毁时,调用析构函数,又一次试图回收这片空间,出现double free问题
自定义拷贝构造函数
Computer(const Computer& ref):_brand(ref._brand), _price(ref._price) {strcpy(_brand, ref._brand);}
拷贝构造调用时机
拷贝构造函数的形参问题
为什么Point(const Point &rhs)不可以换成Point(const Point rhs) ?
const Point &rhs=pt是引用,rhs还是pt的空间
而const Point rhs=pt是赋值,是将pt的内容复制给rhs,本身就会触发拷贝构造的第二种调用时机,会无限循环
什么是左值,什么是右值?
//能够取地址的叫左值,不能取地址的叫右值
//右值包括:临时变量(匿名变量),临时对象(匿名对象) ,字面值常量
int a=1,b=2;
&a;&b
&(a+b) 右值error &1 右值error
Point pt(1,2);
&pt;
Point(2,4).print();//生命周期只在当前行
&Point(2,4) 匿名对象,右值error
//非const 引用只能绑定左值,不能绑定右值
int &ref=a; int &ref2=b;
int &ref3=a+b; 临时变量,右值error
Point &pref=pt;
Point & pref2=Point(2,4); 临时对象,右值error
//const引用既可以绑定左值,又可以绑定右值
const &ref5=a;
const &ref6=a+b;
const & pref3=Point(5,6);
Point(const Point &rhs)可以换成Point( Point &rhs) 吗?
最好不要。
1.常引用是无法改变对象的内容的,拷贝构造的作用是复制函数,若去掉const则可能在函数内改变原对象内容
2.const Point &ref可以用在右值上,而Point & ref只能用在左值上
赋值运算符函数
int x=1,y=2;
x=y;//赋值操作
class Point {
public:Point(int x,int y):_x(x),_y(y){}Point(const Point &ref):_x(ref._x),_y(ref._y){}Point &operator=(const Point & ref)//赋值运算符函数//operator+运算符 都是运算符重载函数{_x = ref._x;_y = ref._y;//return ??}
private:int _x;int _y;
};
void test() {Point pt(10, 8);Point pt2(4, 5);pt = pt2; //本质是pt.operator=(pt2);
}
this指针
class Point {
public:
Point(int x,int y)
:_x(x),_y(y)
{}
Point(const Point &ref)
:_x(ref._x),_y(ref._y)
{}
Point &operator=(const Point & ref)//赋值运算符函数
//operator+运算符 都是运算符重载函数
{
_x = ref._x;
_y = ref._y;
return *this //返回本对象(调用赋值运算符函数的对象--pt)
}
void print() {
cout << "_x=" << this->_x << endl;
cout << "_y=" << this->_y << endl;
}
private:
int _x;
int _y;
};
void test() {
Point pt(10, 8);
Point pt2(4, 5);
pt = pt2; //本质是pt.operator=(pt2);
}
this指针的常识:
1.this指针记录的是本次调用成员函数的对象的地址
2.在成员函数中通过this指针访问"本对象"的成员
3.this指针的类型:Point * const this
4.this指针不要显式的在参数列表中写出
5.编译器会默认的将this指针加入,作为成员函数的第一个参数
(print(),Computer(),~Computer()等第一个参数都是Point* const this)this指针存在哪儿:
编译器在生成程序时加入了获取对象地址的相关代码,将获取的首地址放在了寄存器中,所以无法取this地址
this指针的生命周期:
非静态成员函数被调用,this指针被自动设置为指向调用该函数的对象实例this指针生命周期开始于成员函数的执行开始,this指针的生命周期结束于成员函数执行结束
赋值运算符函数不够用
开辟出堆空间的数据成员会出现浅拷贝类似的错误?
当对象pc和pc2都开辟了堆空间,将pc2的内容赋值给pc,默认的赋值运算符函数只是将pc2指向pc,这时程序结束(作用域结束),自动执行两次析构函数,先销毁pc2,再销毁pc
但实际上只有一个堆空间,会导致错误(类似浅拷贝问题)
如何解决上述浅赋值问题?
自定义赋值运算符函数,对于对象中申请堆空间的数据成员不能只进行浅拷贝,要重新开辟堆空间,开辟堆空间前要先delete原空间
Computer & operator=(const Computer & ref)
{delete []_brand; //回收原本的空间_brand=new char[strlen(ref._brand)+1]();strcpy(_brand,ref._brand);_price=ref._price;return *this;
}
当进行pc=pc自我赋值时,上述自定义赋值运算符函数又会出现问题,怎么解决?
要先判断一下是否是自复制情况,若两个变量地址是相同的就是自复制,是自复制就什么都不干直接返回,若不是自复制才进行操作
Computer & operato=(const Computer & ref){if(this !=&ref){ //this:本对象地址 &ref:对引用取地址就是引用绑定的对象的地址delete []_brand;_brand=new char[strlen(ref._brand)+1]();strcpy(_brand,ref._brand);_price=ref._price;}return *this; //返回本对象
}
赋值运算符函数总结:
1.考虑自复制问题;
2.回收左操作数的数据成员原本申请的堆空间
3.深拷贝(以及其他的数据成员的复制)
4.返回*this(本对象)
赋值运算符返回值和形参问题
赋值运算符函数返回值去掉&可以吗?
可以,但返回的是本对象的副本且不能进行&(pc=pc2),因为临时对象不能取地址
若将返回值改为void有什么问题?
不能进行连续赋值--->pc=pc2=pc3 ,pc2=pc3赋值后是void而不是Point&
赋值运算符函数中形参的&可以去掉吗?
倒也还好,去掉引用&后变成const Computer rhs=pc2的值传递,会调用一次无用的拷贝构造,其余不会造成影响
可以去掉形参中的const吗?
常引用变成普通的引用:
1.可能导致对象(右操作数)内容被修改,不合理
2.pc=Computer("xiaomi",6000);这是赋值操作,但去掉形参中的const则会报错,因为Computer("xiaomi",6000)是右值,只有const引用可以绑定右值,非const引用不能绑定右值
5.三合成原则
析构函数,拷贝构造函数,赋值运算符函数其中一个需要手动定义(不够用的话),另外两个也需要手动定义
6.特殊的数据成员
常量数据成员
常量数据成员不能进行赋值(修改值)操作,除了在构造函数和拷贝构造函数中可以对const数据成员进行初始化操作
引用数据成员
对象成员
class Line {
public:Line(int x1, int y1, int x2, int y2) //初始化列表调用了Point的有参构造//如果没有显式的调用Point的构造函数初始化_pt1,_pt2//会自动调用Point的无参构造(调用形式上无参)//如果Point没有无参构造函数,会直接报错:_pt1(x1,y1),_pt2(x2,y2){}
private://此处声明了两个Point类型的对象成员//创建一个Line对象,在对象的内存布局中会包含两个Point类型对象,称为成员子对象Point _pt1;Point _pt2;
};
对象创建和销毁的顺序(构造函数和析构函数顺序相同)
创建Line函数马上调用Line的构造函数
在Line构造函数执行过程中调用Point的构造函数
在Line对象要销毁就马上调用Line的析构函数
Line的析构函数执行完之后,再根据对象成员声明的反序
通过成员子对象调用Point的析构函数
此处pt2调用析构函数,执行完后pt1调用析构函数
静态数据成员
静态数据成员存储在全局/静态区,不占用对象的存储空间,被整个类的所有对象共享
class Computer{
public://...
private:int _x;static int _y;
};
int Computer::_y=0;//静态数据成员初始化
void test0(){Computer pc(1,2);Computer pc2(3,4);Computer::_y;//pc._y和pc2._y是一个东西,所有对象共有
}
静态数据成员规则:
1.private静态数据成员无法在类外直接访问
2.静态数据成员的初始化,必须放在类外!!!
3.静态数据成员初始化时不能在数据类型前加static,而是在数据成员名前面要加上类名,进行作用域限定
4.如果有多余的静态数据成员,那么他们的初始化需要与声明顺序一致(规范)
5.public静态成员在类外访问时可以通过对象访问,也可以使用 类名::成员名 形式(常用)
6.静态数据成员不占对象的内存结构,而是被整个类所共享
7.特殊的成员函数
静态成员函数
1.静态成员函数不依赖于某一个对象
2.静态成员函数可以通过对象调用,但更常用的方式是类名前加上作用域限定符调用
3.静态成员函数没有this指针
4.静态成员函数中无法直接访问非静态的数据成员,只能访问静态数据成员或调用静态成员函数(因为没有this指针),构造函数,拷贝构造函数,赋值运算符函数,析构函数比较特殊,可以在静态成员函数中调用
注:但非静态的成员函数可以访问静态成员
静态成员函数不能构造函数/拷贝构造函数/析构函数/赋值运算符函数 (因为这四个函数都会访问所有的数据成员,static成员函数没有this指针)
const成员函数
public://this指针原本是Point* const this//const成员函数的this指针变成了const Point* const thisvoid print() const{''''}
特点:
1.const成员函数中,不能修改对象的数据成员
2.当编译器发现该函数是const成员函数时,会自动将this指针设置为双重const限定的指针
8.对象的组织(类比内置类型)
const对象
const对象只能调用const成员函数和数据成员,除了四大金刚
若成员函数没有加const(void print() const{}),即便里面没有_ix=100修改值,也不能pt2.print()访问,因为是const Point pt2(3,5)--->对象不可修改,但pt2.print()不是const成员函数,即便没修改也可能或者说可以修改
const成员函数和const数据成员的区别?
如上图const成员函数中存在char* _brand数据成员,这时const成员函数限定的是_brand不能修改(是地址),但_brand[0]='X'内容可以修改,要想_brand指向内容不可修改,将数据成员改为const char* _brand,但strcpy()等不能用,故最好自己避免修改