C++学习笔记:类和对象

类和对象

面向对象的编程思想

面向过程:数据和逻辑是分离的、独立的,程序世界本质是过程,数据作为过程处理对象,逻辑作为过程的形式定义,世界就是各个过程不断进行的总体。
面向对象的编程思想:数据和逻辑不分离,相互依存。相关的数据和逻辑形成个体,这些个体叫做对象。

类和对象

C++用类来描述对象。
类的定义分为两个部分:
1.数据,相当于现实世界的属性,称为数据成员。
2.对数据的操作,相当于现实世界中的行为,称为成员函数。
有些地方,会将类的数据成员和成员函数统称为类的成员。
类和对象:

  1. 对象就是一个一个的实体、实例
  2. 类就是对于一系列示例的抽象总结

三个特性

  1. 封装:里面的数据成员、成员函数可以有针对性地对外暴露。
  2. 继承:子类可以继承父类的特性,沿用父类的特性
  3. 多态:同一段代码,在不同的环境下,产生的结果是不同的。

基础语法

类和对象之间的关系

类是众多对象的共性地抽象、抽提总结(车、模板)
对象是由类实例化产生的(自行车、摩托车、制作钞票)
从程序设计的角度去看待问题,类其实就是用户自定义的一个数据类型,比如:int、double、Student
类的定义:
类名遵循大驼峰规则

//使用关键字class来标识是一个类
//后面是类名,一般建议是大写,对象、变量使用小写
//后面需要跟随着一组大括号,表示的是类所属的这块区域
class Person{

};
//建议:成员函数写在上部;数据成员写在下部
//为了能够区分是数据成员还是函数里面的形参
//一般数据成员以_开头;形参直接编写对应的名称即可
class Computer{
public:
	void setBrand(const char *brand);
	void strcpy(int price);
	
private:
	char *_brand;
	int _price;
};

访问修饰符/访问权限

pubilc:公有的,无论是在类的内部还是类的外部均可以自由访问
protected:在当前类和当前类的子类中可以自由访问,但是类的外部依然访问不到
private:私有的,仅类的内部可以自由访问,外部是无法访问到的
struct与class的对比:
struct的默认修饰符是public,class的默认修饰符是private。
其他没有区别

成员函数

类的内部定义:

class Computer{
public:
	void setBrand(const char *brand){
		strcpy(_brand,brand);
	}
	void setPrice(float price){
		_price = price;
	}
private:
	char _brand[20];
	int _price;
};

类的内部声明,外部实现:
当成员函数偏多时,可以采取这种方式,好处是可以直接看出具有哪些成员函数


class Computer{
public:
	void setBrand(const char *brand);
	
	void setPrice(int price);
private:
	char _brand[20];
	int price;
}
//放到外部去实现
//成员函数需要使用类名::标识当前的函数属于类的成员函数
void Computer::setBrand(const char *brand){
	strcpy(_brand,brand);
}
void Computer::setPrice(int price){
	_price = price;
}

类的声明和定义放置在两个不同的文件头文件和实现文件
一般是在编写项目或者工程性代码时(比较正式的场景),一般采取这种方式,解耦。

构造函数

构造函数的作用:完成数据成员的初始化工作。
数据成员的初始化工作不应该放在成员函数中,因为在成员函数中的其实是赋值操作,并不是初始化操作。
建议数据成员的初始化放到构造函数中。
构造函数被调用的时机:在当前对象创建的时候构造函数被调用,而且只会被调用一次。
形式:没有返回值,函数名与类名相同,再加上函数参数列表。
特征:
1.当类中没有显式定义构造函数时,编译器会自动生成一个默认(无参)构造函数,但并不会初始化数据成员。
2.当类中显式提供了构造函数时,编译器就不会再自动生成默认的构造函数。
3.构造函数也可以接收参数,在对象创建时提供更大的自由度
4.构造函数可以形成函数重载。
初始化列表:构造函数函数体内的操作认为是赋值操作,不能够算是初始化。
形式:构造函数形参列表之后,函数体之前,用 : 开始,如果有多个数据成员,用 , 分隔,初始值放在一对小括号中。

Point(int x,int y)
:_x(x)//()里面表示需要初始化的值
,_y(y)
{
	cout << "Point(int,int)" << endl;
}
//还可以再写一个无参构造函数
//构造函数可以形成函数重载
Point(){

}

数据成员的初始化并不取决于其在初始化列表中的顺序,而是取决于声明时的顺序

对象空间大小

类里面的成员函数并不会影响到类对象的空间大小,成员函数所存储的内存区域(程序代码区)和类对象所存储的区域不是一个地方。
由于内存对齐的因素,对象所占空间大小是大于等于数据成员之和的。
内存对齐规则如下:
1.第一个数据成员放在offset偏移量为0的地方,后续的每个数据成员需要放置在自身数据成员长度的位置,比如int所占4字节,那么可以存放偏移量为0、4的位置,比如double所占8字节,那么可以存放偏移量为0、8等位置。
2.在数据成员完成各自对齐之后,结构体/类本身也要进行对齐,按照最大数据成员的大小进行对齐。
3.如果一个结构体里有某些结构体成员,则内部结构体成员要从成员最大元素大小的整数倍开始存储。

指针数据成员

指针数据成员的使用有两种办法:浅拷贝和深拷贝。

浅拷贝

浅拷贝:将一个指针数据成员的值赋值给另外一个指针数据成员(抛开const限定的一些因素)。
此时,两个指针数据成员指向的是同一片空间。
当指针指向的是常量区时,无法修改指针所指向的 内存区域的值。
但两个指针指向的是堆区时,在销毁的时候存在风险。
double free: 一个指针进行了堆空间的回收,另外一个指针进行了同样的操作。
一个指针回收了堆空间,另外一个指针正常使用,此时就会出现无法使用的情况。

深拷贝

深拷贝:首先去申请空间,完成对应的拷贝过程,两个指针分别指向不同的内存区域但是两个区域的值是相同的。

class Computer{
public:
	Computer(const char *brand,float price)
	:_brand(new char [strlen(brand) + 1]())
	,_price(price)
	{
		//形参传递的字符串拷贝到申请的堆空间
		strcpy(_brand,brand);
	}
	
	void print(){
		cout << "_brand=" << _brand << ",_price" << _price << endl;
	}
private:
	char *_brand;
	float _price;
}

思考:在构造函数中,进行初始化操作的时候,使用了new表达式,现在存在内存泄漏的问题,该如何解决?
使用析构函数。

析构函数

特性

构造函数和析构函数其实是非常接近的两个函数,
构造函数会在当前对象被创建的时候调用,只会被调用一次。
析构函数会在当前对象被销毁的时候调用,也只会被调用一次。

功能作用

析构函数用来清理对象的数据成员申请的资源(堆空间),析构函数并不负责去清理数据成员。

//特别注意:析构函数可以像普通成员一样调用。
void test(){
	Computer c("APPLE",12000);
	c.~Computer();
	c.print();
}

调用析构函数会导致对象的销毁吗?
对象销毁的时候会调用析构函数,并不是调用析构函数会导致对象的销毁。

形式

和构造函数非常相似,只不过比构造函数的形式要求更为严格一些。
析构函数的函数名称也是与类的名称相同,但是前面有一个~。
析构函数也是没有返回值,连void也没有
析构函数的形参是无参的,没有函数重载的形式。

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;
		}
	}

private:
	char *_brand;
	double _price;
};
void test(){
	Computer c("APPLE",12000);
	c.print;
}

构造阶段:
1.会创建一个Computer c栈对象,创建该对象会调用构造函数,执行构造函数的逻辑。
2.首先给_brand指针数据成员申请堆空间,将申请赋值给指针变量,随后给_price赋值
3.执行构造函数的函数体部分,将Apple的值拷贝到堆空间
析构阶段:
1.会调用析构函数,首先回收指针数据成员申请的堆空间,其次将指针置为nullptr,断开指向。
2.将所有的数据成员申请的堆空间全部回收之后,出作用域,系统会将当前的C对象进行回收。

不同类型的对象的析构函数的调用时机

析构函数的调用时机:程序、函数作用域
全局对象:
构造函数会在程序启动时调用。
析构函数会在整个应用程序结束的时候调用。
局部对象:析构函数会在当前函数作用域结束时调用。
静态对象:析构函数会在整个应用程序结束的时候调用。
堆对象:何时调用delete操作,那么何时执行析构函数。

拷贝构造函数

void test2(){
	Point pt1(1,2);
	Point pt2 = pt1;
	pt2.print();
}

pt2的创建其实是利用了拷贝构造函数创建的。

形式

//编译器提供默认的拷贝构造函数的实现
//其中rhs变量的名称相对比较固定,表示的是右操作数
//对应的lhs表示的是左操作数

类名 (const 类名 &)
Point(const Point & rhs)
:_ix(rhs._ix)//浅拷贝
,_iy(rhs._iy)
{
	cout << "point(const Point &)"<<endl;
}
Point(const Point &rhs)
:_ix(new char [strlen(rhs._brand)+1]())//深拷贝
,_iy(rhs._iy)
//如果设计指针数据成员,建议使用深拷贝的形式,显示的将拷贝构造函数写出,编译器提供的默认拷贝构造函数使用的是浅拷贝

Point pt1(1,2);
//pt1就是属于右操作数,pt2属于左操作数
//下面两种写法都表示的是拷贝构造函数
Point pt2 = pt1;
Point pt3(pt1);

形式探究

引用符号为什么不可以去掉:首先编译器会直接报错,其次拷贝构造函数的形参是一个对象,调用拷贝构造函数的时候,实参和形参相结合会进一步调用拷贝构造函数;调用拷贝构造函数,就需要形参和实参相结合,此时就又要调用拷贝构造函数。
const的意义:
const如果去掉则无法处理下面的场景

void test(){
	Point pt = Point(1,2);
}

原因在于形参和实参结合时Point & rhs =Point(1,2)是不合理的,加上const以后,const引用可以绑定左值,也可以绑定右值,会将右值的生命周期提升到和引用变量相同的生命周期
关于左值、右值、左值引用、const引用:
左值是可以取地址的,右值是不可以取地址操作的,临时变量、临时对象
左值和右值还可以去记忆是否有名称
一般情况下有名称的是左值,没有名称的是右值
"hello,world"字符串常量是左值,可以取地址

int number = 10 ;
int & ref = number;//左值引用,绑定的是一个左值
int & ref2 = 10;//ref2是一个左值引用,只可以去绑定左值,无法绑定右值

const引用、非const引用:

const int & ref4 = number;
const int & ref5 = 10;
//const引用可以提升右操作数也就是右值的生命周期
const Point & refPoint2 = Point(1,2);

拷贝构造函数的调用时机

1.使用一个已经存在的对象去创建一个新的对象

class Point{
public:
	//构造函数
	Point(int x,int y)
	:_x(x)
	,_y(y)
	{
		
	}
	Point(const Point &rhs)
	:_x(rhs._x)
	,_y(rhs._y)
	{
		cout <<"Point(const Point &)" << endl;
	}
private:
	int _x;
	int _y;
};
void test(){
	Point pt(1,2);
	//pt2和pt3都调用了拷贝构造函数
	//使用一个已经存在的对象去创建一个新的对象
	Point pt2 = pt;
	POint pt3(pt);
}

2.作为函数参数(实参和形参的类型都是对象),形参与实参相结合时(实参初始化形参)

void func(Point pt){
	cout << &pt << endl;
	pt.print();
}
void test2(){
	Point pt1(2,3);
	cout << &pt1 << endl;
	func(pt1);
}

3.作为函数的返回值,函数的返回值是一个对象,执行return语句的时候

Point globalPoint(1,2);
Point func3(){
	return globalPoint;
}
Point func4(){
//执行func4函数的时候,构造函数会被调用两次
//全局的对象依然会被创建
//默认情况下没有拷贝构造函数的调用
//编译器有一个优化机制,可以加上去优化指令
//-fno-elide-constrcutors --std=c++11
	Point localPoint(3,4);
	return localPoint;
}

赋值运算符函数

赋值操作和拷贝构造的区别在于左操作数前面是否有数据类型。

Point pt1(1,2),pt2(3,4);
pt1 = pt2;//赋值操作
pt1 = pt2
#if 0
经过编译器处理之后变成了 pt1.operator(pt2);
rhs其实就是pt2
赋值运算符的返回值Point &返回的是pt1
so:pt1.operator(pt2);与上面的写法完全等价
#endif
Point pt = pt2;//拷贝构造

赋值运算符的形式

类名 & operator = (const 类名 & rhs)

Point & operator = (const Point & rhs){
	_x = rhs._x;
	_y = rhs._y;
	//返回值写什么?
	//this指针
	return *this;
}

类对象的大小只和数据成员相关,对象可能在栈区、堆区、也可能在静态区,但是成员函数位于程序代码区。

this指针

this指针:this指针是给类中的成员函数使用的,当类对象调用函数时,通过this指针来找到自己的数据成员,,它的类型为Type * const this,this指针指向了本对象,不可以修改指向。
成员函数存在一个隐形的参数,这个参数便是this指针。
编译器会将成员函数进行解析处理,会多出来一个隐形的this指针形参,该形参的类型叫做Type * const this对于该指针来说,不可以变更指向,但是可以修改指向的值。

void print(){
	cout << _x << "," << _y << endl;
}
#if 0
经过编译器处理后的print函数
void print(*this){
	cout << this -> _x << "," << this -> _y << endl;
}

#endif

理解以下问题:
1.类对象调用函数时,是如何找到自己本对象的数据成员的?通过this指针
2.this指针代表的是什么?指向本对象
3.this指针在参数列表中的什么位置?参数列表的第一位(默认自动加入,不用手动写出)
4.this指针的形式是什么?类名 * const this

编译器在生成程序时加入了获取对象首地址的相关代码,将获取的首地址存放在了寄存器中,这就是this指针。
this指针的生命周期开始于成员函数的执行开始,结束于成员函数的执行结束。

#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
class Point{
public:
    Point(int x,int y)
    :_x(x)
    ,_y(y)
    {
        cout << "Point(int,int)" << endl;
    }
    Point(const Point & rhs)
    :_x(rhs._x)
    ,_y(rhs._y)
    {
        cout << "Point(const Point & rhs)" << endl;
    }
    ~Point(){
        cout << "~Point()" << endl;
    }
    Point & operator = (const Point & rhs){
        _x = rhs._x;
        _y = rhs._y;
        cout << "Point & operator = (const Point & rhs)" << endl;
        return *this;
    }

    void print(){
        cout << _x << ","<< _y << endl;
    }
private:
    int _x;
    int _y;
};
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;
        }
    }

    Computer(const Computer & rhs)
    :_brand(new char[strlen(rhs.brand +1)]())
    ,_price(rhs._price)
    {
        strcpy(_brand,rhs.brand);
    }

    Computer & operator = (const Computer & rhs){
        //左操作数应当先执行堆空间的回收操作
        //再去执行深拷贝操作
        if(this != &rhs){
            delete [] _brand;
            _brand = new char[strlen(rhs._brand)+1]();
            strcpy(_brand,rhs._brand);
            _price = rhs._price;
        }
        return *this;
    }
    
    void print(){
        cout << "_brand = " << _brand << ", _price = "
             << _price << endl;
    }
private:
    char *_brand;
    double _price;
};
void test(){
    Computer c1("APPLE",12000),c2("HUAWEI",8000);
    c1.print;
    c2.print;
    c1 = c2;
    c1.print;
    c2.print;
    //极端情况
    c1 = c1;
#if 0
    第78行
    调用赋值操作运算符函数,在使用浅拷贝的时候
    会发生内存泄漏和double free 的问题
    因次,我们应该先将c1的_brand先回收掉,再重新开辟空间,进行复制
#endif
}

int main()
{
    test();
    return 0;
}

四步走

1.自赋值判断
2.回收左操作数申请的堆空间
3.执行深拷贝操作
4.返回当前对象

赋值运算符函数的形式探究:
类名 & operator = (const 类名 & rhs)
Q1.返回值可以不是引用吗?
建议返回一个引用,否则会满足拷贝构造函数的调用时机,再拷贝一次
Q2.返回值可以是void吗?
返回值如果是void没法去处理连续赋值的场景。
Computer c1("Apple",1200),c2("Huawei",7800),c3("Mi",3000)
Q3.赋值运算符的参数一定要求是一个引用吗?
如果不是引用,会调用一次拷贝构造函数
Q4.形参一定要求const吗?是,否则没法处理右值的赋值操作。

三合成原则:析构函数、拷贝构造函数、赋值运算符函数,如果因为业务重写了其中的一个,那么另外两个也得重写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值