文章目录
第二章 类和对象
一、C++中的类
1.类的定义
C++中用关键字class来定义一个类,其基本形式如下:
class 类名
{
public: //公有数据成员和成员函数
protected: //保护数据成员和成员函数
private: //私有数据成员和成员函数
}; // 千万不要忘了这个分号
public进行修饰的成员表示的是该类可以提供的接口、功能、或者服务;
protected进行修饰的成员,其访问权限是开放给其子类;
private进行修饰的成员是不可以在类之外进行访问的,只能在类内部访问,可以说封装性就是由该关键字来体现。
在类中定义的成员函数,都是 inline 函数。除了可以在类内部实现外,成员函数还可以在类之外实现。在类定义的外部定义成员函数时,应使用作用域限定符(::)来标识函数所属的类,即有如下形式:
返回类型 类名::成员函数名(参数列表)
{
//....
}
//类的实现:成员函数的实现
class Computer//类的内部:大括号以内
{
public://对外的接口(服务)
//成员函数
//默认情况,在类内部实现的成员函数是内联函数
inline
void setBrand(const char* brand)
{
strcpy(_brand,brand);
}
inline
void setPrice(float price)
{
_price=price;
}
inline
void print()
{
cout << "brand : " << _brand << endl;
<< "price : " << _price << endl;
}
private: //封装,不能再类的外面使用
//成员数据
char _brand[20];
float _price;
};
int main()
{
Computer pc;
pc.setBrand("Lenovo");
pc.setPrice(6900);
pc.print();
strcpy(pc._brand,"Mac"); //(❌)error private只能类内部访问
pc._price=15000; //(❌)error private只能类内部访问
pc.print();
return 0;
}
2.class与struct的区别
//C++对struct关键字做了提升,与class类似(里面可以实现函数)
//struct成员函数,访问权限是public;class成员函数,访问权限是private
struct Computer
{
void setBrand(const char* brand)
{
strcpy(_brand,brand);
}
void setPrice(float price)
{
_price=price;
}
void print()
{
cout << "brand : " << _brand << endl;
<< "price : " << _price << endl;
}
char _brand[20];
float _price;
}
二、对象的创建与销毁
1.对象的创建(注意初始化)
构造函数在创建对象时被系统自动调用,C++ 为类提供了一种特殊的成员函数----构造函数来对数据成员进行初始化。构造函数有一些独特的地方:
- 函数的名字与类名相同
- 没有返回值
- 没有返回类型,即使是 void 也不能有
class Point
{
public:
//默认情况(当我们没有自定义构造函数),编译器也会自动提供一个空函数
//Point()
//{
//}
//构造函数是支持重载的
Point()
: _ix(0) //初始化表达式(列表),它才是对数据成员进行真正的初始化
, _iy(0) //初始化列表中的数据成员的初始化,只与数据成员声明的时候的顺序有关
{ //不能先初始化_iy,再初始化_ix
cout << "Point()" << endl;
_ix = 0; //赋值
_ix = 0;
}
//构造函数的作用:为了初始化数据成员
Point(int ix, int iy)
: _ix(ix) //初始化
, _iy(iy)
{
cout << "Point(int , int)" << endl;
_ix = ix; //赋值
_iy = iy;
}
void print()
{
cout << "ix : " << _ix << endl;
<< "iy : " << _iy << endl;
}
private:
int _ix;
int _iy;
}
int main()
{
//Point pt; 当我们有自定义的构造函数时,系统提供的构造函数就不能使用了
Point pt(1,2); //对象在创建的时候,调用了构造函数
pt.print();
return 0;
}
2.对象的销毁
析构函数(Destructor)对象被撤销时被自动调用,析构函数有如下特点:
- 与类同名,之前冠以波浪号,以区别于构造函数。
- 析构函数没有返回类型,也不能指定参数。因此,析构函数只能有一个,不能被重载。
- 对象超出其作用域被销毁时,析构函数会被自动调用。
class Point
{
public:
//...
//析构函数:没有函数返回值,没有参数,不能重载,具有唯一性
//默认情况,编译器自动生成
//析构函数在main函数中可以显示调用,但是不建议这么做
~Point()
{
}
//...
};
析构函数清理资源-实例:
class Computer
{
public:
Computer(const char * brand, double price)
//一个指针直接赋值给另一个指针,这两个指针指向同一位置,一个修改另一个也跟着变化
: _brand(new char[strlen(brand) + 1]()) //需要先申请空间,再拷贝数据
, _price(price)
{
strcpy(_brand, brand)
}
~Computer() //析构函数可以显示调用,但是不要这么操作
{
if(_brand)
{
delete [] _brand; //清理对象中成员的资源
_brand = nullptr; //C++中尽量不用NULL
}
cout << "~Computer()" << endl;
}
private:
char * _brand;
double _price;
};
析构函数调用时机:
- 对于全局定义的对象,每当程序开始运行,在主函数 main 接受程序控制权之前,就调用构造函数创建全局对象,整个程序结束时,自动调用全局对象的析构函数。
- 对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数。
- 对于关键字 static 定义的静态局部对象,当程序流程第一次到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。
- 对于用 new 运算符创建的对象,每当创建该对象时调用构造函数,当用 delete 删除该对象时,调用析构函数。
三、拷贝构造函数
1.深拷贝、浅拷贝
//原型:
类名::类名(const 类名&);
Point::Point(const Point & rhs)
: _ix(rhs._ix)
, _iy(rhs._iy)
{
}
实例:
//Q:拷贝构造函数的引用为什么不能去掉?
//A:如果引用去掉,拷贝过程中会无穷递归下去,并且没有出口
//Q:拷贝构造函数的const为什么不能去掉?
//A:如果const去掉,当传递对象(右值)的时候,会报错
//const Point &rhs = func3(); //ok
//Point &rhs = func3(); //error(❌)
Computer::Computer(const Computer & rhs)
: _brand(new char[strlen(rhs._brand) + 1]()) //深拷贝,两个对象有各自独立的堆空间字符串(new char[]())
, _price(rhs._price)
{
strcpy(_brand, rhs._brand);
}
Computer::Computer(const Computer & rhs)
: _brand(rhs._brand) //浅拷贝,两个对象指向同一个堆空间的字符串
, _price(rhs._price)
{
}
//执行构造初始化
Computer pc1("Huawei Matebook14", 5699);
Computer pc2 = pc1;
拷贝构造函数的调用时机:
- 当把一个已经存在的对象赋值给另一个新对象时,会调用拷贝构造函数。
- 当实参和形参都是对象,进行实参与形参的结合时,会调用拷贝构造函数。
- 当函数的返回值是对象,函数调用完成返回时,会调用拷贝构造函数。
附:左值、右值
左值:可以取地址
右值:不可以取地址(临时对象/匿名对象func4() 字面值常量10),因为其生命周期只在本行,不是一个持久化的量(对象),在内存里面没有存起来,所以不能取地址
四、隐含的this指针
在类中定义的非静态成员函数中都有一个隐含的this指针,它代表的就是当前对象本身,它作为成 员函数的第一个参数,由编译器自动补全.
//实质:指向对象本身
//存在位置:隐含在成员函数的第一个参数处
void print( /* Point * const this */)
{
// this =nullptr; (❌)error,this是常量指针
this->ix = 0;
cout << "(" << this->ix
<< "," << this->iy
<< ")" <<endl;
}
void test
{
Point pt1(1, 5);
pt1.print(); //pt1.print(&pt1); 把对象的地址传入
}
五、赋值运算符函数
Point pt1(1,2),pt2(3,4);
//在执行pt1=pt2;该语句时,pt1与pt2都存在,所以不存在对象的构造,这要与Point pt2=pt1;
pt1 = pt2; //对象赋值操作(两个已经定义了的对象),需要赋值运算符函数
//实质形式:
返回类型 类名::operator=(参数列表)
{
//......
}
//没有显式定义赋值运算符函数时,编译器会自动提供一个缺省的赋值运算符函数
//Q:赋值运算符函数返回值的引用能否去掉?
//A:不能去掉,否则在执行赋值操作的时候,会满足拷贝构造函数的调用时机3,会执行拷贝构造函数,效率就降低了,开销大了
//Q:赋值运算符函数返回值类型能不能是void?
//A:不能返回void,否则不能执行连等操作 pt1 = pt2 = pt3
Computer & Computer::operator=(const Computer & rhs)
{
_brand = rhs._brand;
_price = rhs._price;
return *this; //若返回类型为A, 则是克隆, 若返回类型为A&,则是本身
//return this返回当前对象的地址(指向当前对象的指针)
}
赋值运算符函数的四部曲:
Computer & Computer::operator=(const Computer & rhs)
{
//赋值运算符函数:四部曲
if(this != &rhs) //1.自复制的情况去除
{
delete [] _brand; //2.释放左操作数原先的堆空间
_brand = new char[strlen(rhs._brand) + 1](); //3.执行深拷贝
strcpy(_brand, rhs._brand);
_price = rhs._price;
}
return *this; //4.返回*this
}
六、特殊数据成员的初始化
在 C++的类中,有4种比较特殊的数据成员,他们分别是常量成员、引用成员、类对象成员和静态成员,他们的初始化与普通数据成员有所不同。
1.常量数据成员
当数据成员用const关键字进行修饰以后,就成为常量成员。一经初始化,该数据成员便具有“只读属性”,在程序中无法对其值修改。事实上,在构造函数体内初始化 const 数据成员是非法的,它们只能在构造函数初始化列表中进行初始化。如:
class Point {
public:
//错误写法
Point(int ix = 0, int iy = 0)
{
_ix = ix; //(❌)error, 这是赋值语句,const成员不能修改
_iy = iy; //(❌)error, 这是赋值语句,const成员不能修改
_iz = _ix;
}
//正确写法
Point(int ix = 0, int iy = 0)
: _ix(ix)
, _iy(iy)
, _iz(_ix) // int & _iz = _ix (引用初始化)
//, _iz(0) (❌)error, int & _iz = 0; 语法错误
{
}
private:
const int _ix;
const int _iy;
int & _iz; //引用数据成员,需要在初始化表达式中进行初始化
//引用作为数据成员的时候,占用一个指针大小的空间
};
2.引用数据成员
见上:
3.类对象成员
当数据成员本身也是自定义类类型对象时,比如一个直线类Line对象中包含两个Point类对象,对Point对象的创建就必须要放在Line的构造函数的初始化列表中进行。如
class Line
{
public:
Line(int x1, int y1, int x2, int y2)
: _pt1(x1, y1) //一个自定义的类型创建的对象作为另外一个自定义类型的数据成员
, _pt2(x2, y2) //默认会调用子对象的默认(无参)的构造函数,
{
cout << "Line(int,int,int,int)" << endl;
}
private:
Point _pt1;
Point _pt2;
};
当Line的构造函数没有在其初始化列表中初始化对象_pt1和_pt2时,系统也会自动调用 Point类的默认构造函数,此时就会与预期的构造不一致。因此需要显式在Line的构造函数初始化列表中初始化_pt1和_pt2对象。
4.静态数据成员
C++允许使用static(静态存储)修饰数据成员,这样的成员在编译时就被创建并初始化的(与之相比,对象是在运行时被创建的),且其实例只有一个,被所有该类的对象共享
class Computer {
public:
Computer(const char * brand, double price)
: _brand(new char[strcpy(brand) + 1]())
, _price(price)
//, _totalpriceK(0) (❌)error,静态数据成员不能在类内初始化
{
_totalPrice += _price;
}
void print()
{
cout << "品牌:" << _brand << endl
<< "价格:" << _price << endl
<< "总价:" << _totalPrice << endl;
}
//...
private:
char * _brand;
double _price; //内存对齐 换成float之后占用的内存大小是一样的
static double _totalPrice; //静态数据成员不占用类的空间,所有的对象所共享的
//必须在类外面进行初始化
};
//静态数据成员必须放到类外部进行初始化(特别是,对于头文件与实现文件这种形式:必须放在实现文件中)
double Computer::_totalprice = 0;
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类对象时被定义的。这意味着它们不是由类的的构造函数初始化的,一般来说,我们不能在类的内部初始化静态数据成员,必须在类的外部定义和初始化静态数据成员,且不再包含static关键字,格式如下:
类型 类名::变量名 = 初始化表达式; //普通变量
类型 类名::对象名(构造参数); //对象变量
七、特殊的成员函数
除了特殊的数据成员以外, C++类中还有两种特殊的成员函数:静态成员函数和 const 成员函数。
1.静态成员函数
静态成员函数的特点:
- 静态成员函数内部不能使用非静态的成员变量和非静态的成员函数
- 静态成员函数内部只能调用静态数据成员和静态的成员函数
非静态成员函数可以调用静态成员函数
如果一定要在静态成员函数中调用非静态成员函数,可以在静态成员函数中传参(对象),从而调用参数的函数
static 类型 函数名(参数列表)
{
函数体
}
原因在于静态成员函数的参数列表中不含有隐含的 this 指针,如下:
class Computer {
public:
Computer(const char * brand, double price)
: _brand(new char[strlen(brand) + 1]())
, _price(price)
{
_totalPrice += _price;
}
//...
static void printTotalPrice()
{
cout << "总价:" << _totalPrice << endl;
}
//...
private:
char * _brand;
double _price;
static double _totalPrice;
};
对于静态成员函数,还可以直接通过类名进行调用,这也是最常用的方式:
//静态成员函数不用创建对象而能调用类中的函数
int main(void)
{
Computer pc1("Huawei MateBook14", 5699);
pc1.print();
Computer::printTotalPrice(); //通过类名直接调用
return 0;
}
2.const成员函数
const 在类成员函数中还有种特殊的用法,把const关键字放在函数的参数表和函数体之间(与之前介绍的 const放在函数前修饰返回值不同), 称为const成员函数,其具有以下特点:
- 只能读取类数据成员,而不能修改之。
- 只能调用
const成员函数,不能调用非const成员函数。
类型 函数名(参数列表) const
{
函数体
}
class Computer
{
public:
//...
//const版本的成员函数可以与非const的版本的成员函数进行重载
//一般建议先写const版本的成员函数,有需要再写非const版本
//特别地,如果不修改数据成员,就定义const版本的函数
void print()
{
}
void print() const
{
cout << "品牌:" << _brand << endl
<< "价格:" << _price << endl;
}
//...
};
//默认情况下,非const对象可以调用非const版本的成员函数
//非const对象可以调用const版本的成员函数
Computer com1("Thinkpad", 8000);
com1.print();
//const对象可以调用const版本的成员函数
//const对象不能调用非cosnt版本的成员函数
const Computer com2("xiaomi", 6000);
com.print();
八、对象的组织
有了自己定义的类,或者使用别人定义好的类创建对象,其机制与使用int等创建普通变量几乎完全一 致,同样可以创建const对象、创建指向对象的指针、创建对象数组,还可使用 new和 delete等创建动态对象。
1.const对象
const Point pt(1, 2);
pt.print();
2.指向对象的指针
类名 * 指针名 [=初始化表达式];
Point pt(1, 2);
Point * p1 = nullptr;
Point * p2 = &pt;
Point * p3 = new Point(3, 4);
Point * p4 = new Point[5];
p3->print();//合法
(*p3).print();//合法
3.对象数组
类名 数组名[对象个数];
Point pts[2] = {Point(1, 2), Point(3, 4)};
Point pts[] = {Point(1, 2), Point(3, 4)};
Point pts[5] = {Point(1, 2), Point(3, 4)};
//或者
//Point pts[2] = {(1,2),(3,4)}; //(❌)error 注意小括号内的逗号运算符
Point pts[2] = {{1, 2}, {3, 4}}; //注意是大括号
Point pts[] = {{1, 2}, {3, 4}};
Point pts[5] = {{1, 2}, {3, 4}};
4.堆对象
和把一个简单变量创建在动态存储区一样,可以用new和delete表达式为对象分配动态存储区,
void test()
{
Point * pt1 = new Point(11, 12);//单个堆对象 调用构造函数
pt1->print();
delete pt1;
pt1 = nullptr;
Point * pt2 = new Point[5]();//堆对象数组 注意,不能进行赋值
pt2->print();
(pt2 + 1)->print();
delete [] pt2; //new和delete配合,不能用free释放
}
注意: 使用new表达式为对象数组分配动态空间时,不能显式调用对象的构造函数,因此,对象要么没 有定义任何形式的构造函数(由编译器缺省提供),要么显式定义了一个(且只能有一个)所有参数都 有缺省值的构造函数。
九、单例模式
单例模式是23种GOF模式中最简单的设计模式之一,属于创建型模式,它提供了一种创建对象的方式, 确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象。
现步骤有如下三步:
- 将构造函数私有化
- 在类中定义一个静态的指向本类型的指针变量
- 定义一个返回值为类指针的静态成员函数
//单例模式的要求:一个类只能创建一个对象
//不能是栈对象,不能是全局对象,只能是堆对象 栈对象、全局对象系统自动分配空间
//
//用途:全局唯一的资源,全局唯一变量
//字典库、词典库、网页库、日志系统记录日志的对象
class Singleton
{
public:
//静态成员函数可以在类外不创建类的对象而调用类中的函数
static Singleton *getInstance()
{
if(nullptr == _pInstance)
{
_pInstance = new Singleton();//类的内部访问构造函数
}
return _pInstance;
}
static void destroy()
{
if(_pInstance)
{
delete _pInstance; //调用private中的析构函数
_pInstance = nullptr;
}
}
private:
//构造函数私有,不能在类的外面访问
Singleton()
{
cout << "Singleton()" << endl;
}
~Singleton() //析构函数放在private里面,只能类内调用
{
cout << "~singleton()" << endl;
}
private:
//仅是声明,没有定义
//static int *s1;
static Singleton *_pInstance; //静态成员函数需要静态的成员
};
//类外的定义才是给静态成员变量分配空间
// int *s1 = nullptr;
Singleton * Singleton::_pInstance = nullptr;//静态成员进行初始化
//Singleton gS1;//全局对象,error,不能让其编译通过,只能让构造函数私有
//Singleton gS2;//全局对象
int main()
{
//Singleton s1;//栈对象,error,不能让其编译通过,只能让构造函数私有
//Singleton s2;//栈对象
//Singleton *ps1 = new Singleton(); //堆对象。类的外面创建不行,构造函数为私有
Singleton *ps1 = Singleton::getInstance();
Singleton *ps2 = Singleton::getInstance();
cout << "ps1 = " << ps1 << endl
<< "ps2 = " << ps2 << endl;
//delete ps1; //new两次指向同一空间,delete两次的时候会出现两次析构函数
//delete ps2; //把析构函数放入private中,就只能类内调用了
Singleton::destory();
return 0;
}
//静态成员函数需要静态的成员
};
//类外的定义才是给静态成员变量分配空间
// int *s1 = nullptr;
Singleton * Singleton::_pInstance = nullptr;//静态成员进行初始化
//Singleton gS1;//全局对象,error,不能让其编译通过,只能让构造函数私有
//Singleton gS2;//全局对象
int main()
{
//Singleton s1;//栈对象,error,不能让其编译通过,只能让构造函数私有
//Singleton s2;//栈对象
//Singleton *ps1 = new Singleton(); //堆对象。类的外面创建不行,构造函数为私有
Singleton *ps1 = Singleton::getInstance();
Singleton *ps2 = Singleton::getInstance();
cout << "ps1 = " << ps1 << endl
<< "ps2 = " << ps2 << endl;
//delete ps1; //new两次指向同一空间,delete两次的时候会出现两次析构函数
//delete ps2; //把析构函数放入private中,就只能类内调用了
Singleton::destory();
return 0;
}

1933

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



