【C++学习笔记】原课程视频网址为https://www.bilibili.com/video/BV1dVxNefEpx?spm_id_from=333.788.videopod.sections&vd_source=1a59b0ecdb0243bed6579859a87b14b1
1.类的声明
一个C++的类包含了数据和函数两部分,类的声明方式使用class关键字,后面是类的名称,然后是大括号,大括号里是类的成员,包括成员变量和成员函数,其中的public、private和protected是访问控制符。在public下,所有的类成员都是公共类型的,开放给所有其他程序代码使用;在private下所有类成员都是私有的,只能在这个类自己的函数中访问;在protected下允许除了这个类自己的函数可以访问,其继承类也可以访问。如果成员变量和函数之前没写访问控制关键字,那么默认为private,比如下图示例中的成员变量0。在类的声明和定义结束后,在类的右大括号要有一个分号做结束。
在以下例子中,定义了一个名为Shape的类,这个类提供一些与几何形状有关的函数和数据。驼峰命名法:在定义一个类时,习惯使用首字母大写的一个或多个单词作为类的名称,并且单词之间不用下划线分割。
class Shape{
protected:
std::string shapeName;
private:
int positionX, positionY;
public:
float area();
bool draw();
};
以上只是对类进行了声明:只定义了这个类的数据成员和函数接口,类成员函数的具体实现还没有定义。
2.成员函数的定义
成员函数的定义有两种方式。
1.内联方式:在类的声明中直接写出函数的定义
2.单独写出成员函数的定义:成员函数的定义与普通函数类似,注意在函数名称前添加双冒号。
3.类的封装
封装:通过类的定义,隐藏内部数据的细节,当需要获得数据时,通过调用类的方法(类的成员函数)来获得相应的数据,而不必知道这些数据如何存储,因此经常将数据声明为私有的或受保护的,而将类的函数声明为公共的。由于类的成员函数可以访问这个类中的任意成员,这样外部代码就可以通过调用公共的成员函数来读取或者修改数据。
在以下代码中,shapeName为受保护成员,外部代码要获得这个形状名称时需要通过公共的成员函数getName获得。getName返回的是一个const的引用,这说明函数返回的引用只能被读取而不能被修改。当我们要修改时,要使用函数setName,在setName中将传递的参数name的值拷贝给了成员变量shapeName。当我们定义了一个类之后,就可以像使用基本类型那样使用这个类了。
class Shape{
protected:
string shapeName;
private:
int positionX, positionY;
public:
const string& getName(){
return shapeName;
}
setName(const string& name){
shapeName = name;
}
};
Shape shape;//shape 类的实例(类的对象)
shape.setName("形状一");//通过类的对象来调用它的成员函数
cout<<shape.getName()<<endl;
还可以,动态创建一个Shape的对象,并用指针pShape来指向这个对象。当我们要通过指针来访问这个类对象的成员时,使用->操作符(间接成员运算符),动态创建的对象在使用完毕后需要用delete来释放掉。
class Shape{
protected:
string shapeName;
private:
int positionX, positionY;
public:
Shape(int x, int y) : positionX(x), positionY(y) {};
const string &getName(){return shapeName;}
void setName(const string &name){ shapeName = name; }
};
//指针调用
Shape* pShape = new Shape(0,0);
pShape->setName("形状一");
cout<<pShape->getName()<<endl;
delete pShape;//动态创建的对象在使用完毕后需要用delete来释放掉
//智能指针调用
shared_ptr<Shape> pShape = shared_ptr<Shape>(new Shape(0,0));
pShape->setName("形状一");
cout<<pShape->getName()<<endl;
4.构造函数与析构函数
构造函数的函数名与类名相同,构造函数也可以重载(可以有多个不同参数类型或参数数量)。在以下示例中有两个构造函数,第一个是无参数的构造函数,无参数构造函数是类的默认构造函数,如果在定义一个类的时候,一个构造函数也没有定义,编译器就会生成一个什么都不做的无参数构造函数。当定义了一个带参数的构造函数时,编译器就不会再自动生成默认构造函数,这时如果需要没有参数的构造函数就需要自己定义了。第二个构造函数有两个整形参数,这两个构造函数的访问属性是public,所以可以在任意地方调用。
class Shape{
public:
Shape();
Shape(int x, int y);
};
Shape shape1;//调用无参数构造函数 注意不要加小括号 加了括号编译器会认为是声明了一个函数
Shape shape2(5,10);//调用第二个构造函数
拷贝构造函数:拷贝构造函数的参数是一个const类型的同类对象,这种构造函数使用其他对象作为初始化参数
class Shape{
int positionX;
int positionY;
public:
Shape(const Shape& other);
};
//这里有个shape1的对象
Shape shape1;
//采用复制初始化的方式创建了shape2这个对象,创建对象时调用了拷贝构造函数,并使用shape1作为参数
//通常拷贝构造函数会将shape1成员变量的值复制给shape2成员变量
Shape shape2 = shape1;
//以下这几种方式同样会调用拷贝构造函数
Shape shape3(shape1);
Shape shape5 = Shape(shape1);
Shape *pShape = new Shape(shape1);
如果定义一个类时,没有定义拷贝构造函数,那么编译器会自动生成一个构造函数。自己定义拷贝构造函数的原因:有时候默认的拷贝构造函数与期望不符。例如如果类成员中有一个指针,默认的拷贝构造函数会将参考对象的指针拷贝给新创建对象的指针,就像以下例子中的拷贝函数这样,新建的Shape对象的m_pValue指针会与原有Shape对象中的指针指向的是同一块内存区域。我们期望新建的对象和原有的对象保持独立,各管各的,那么就应该重新分配一块内存区域,并将参考对象中指针指向的值复制过来,而不是只复制一个指针(深拷贝、浅拷贝)。注意以下两个例子的区别,第一个为浅拷贝,第二个为深拷贝。
class Shape{
private:
int* m_pValue;
public:
Shape(const Shape& other)
{
m_pValue = other.m_pValue;
}
/*...*/
};
class Shape{
private:
int* m_pValue;
public:
Shape(const Shape& other)
{
m_pValue = new int(*other.m_pValue);
}
/*...*/
};
此外,如果不希望别人通过直接定义变量或者使用new运算符来创建这个类的对象,可以把默认的构造函数放在private下面,这样在这个类之外的代码就不能调用默认构造函数来创建类对象。这样我们在定义一个Shape对象时,编译器就会报错。
class Shape{
int positionX, positionY;
private:
Shape();
};
Shape shape1;//此时会报错!!!
析构函数:析构函数的定义方式是在类名前加一个波浪号,与构造函数不同,析构函数只有一个,不能带参数。析构函数的作用是在这个类对象被销毁之前需要做一些操作,这通常应用在类的一些成员变量是动态创建的指针变量,在类被删除之前,这些内存也需要被释放。如果没有写析构函数,编译器也会写一个默认的析构函数,只不过这个析构函数什么都不做。
class Example{
private:
int* pInt;
public:
Example(){
pInt = new int(2);
}
~Example(){
delete pInt;//释放内存,避免内存泄漏
}
};
为防止代码臃肿,定义类时通常在头文件(.h .hpp或没有后缀)中写类的声明,在源文件中(ccp css cc)写类的函数的定义。
为了防止头文件被包含多次,需要在头文件中增加编译指令,以下保证头文件只被包含一次。