写在前面:
- 本系列专栏主要介绍C++的相关知识,思路以下面的参考链接教程为主,大部分笔记也出自该教程,笔者的原创部分主要在示例代码的注释部分。
- 除了参考下面的链接教程以外,笔者还参考了其它的一些C++教材(比如计算机二级教材和C语言教材),笔者认为重要的部分大多都会用粗体标注(未被标注出的部分可能全是重点,可根据相关部分的示例代码量和注释量判断,或者根据实际经验判断)。
- 由于C++基本继承了C语言的所有内容,建议读者先阅读C语言系列的专栏,有一些重点是仅在C语言系列专栏中有介绍的(比如二级指针、预处理等)。
- 如有错漏欢迎指出。
参考教程:黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难_哔哩哔哩_bilibili
一、类和对象概述
(1)C++面向对象的三大特性为:封装、继承、多态。
(2)C++认为万事万物都皆为对象,对象上有其属性和行为。例如:
①人可以作为对象,属性有姓名、年龄、身高、体重...,行为有走、跑、跳、吃饭、唱歌...
②车也可以作为对象,属性有轮胎、方向盘、车灯...,行为有载人、放音乐、放空调...
(3)具有相同性质的对象,我们可以抽象称为类,例如人属于人类、车属于车类。
二、类和对象的定义
1、类的定义
(1)类的定义可以分为说明部分和实现部分两部分,其中说明部分说明类中包含的数据成员和成员函数,实现部分是对成员函数的定义。
(2)类定义的一般格式如下:
//类的说明部分
class <类名>
{
public:
<成员函数或数据成员的说明> //公有成员,外部接口
protected:
<成员函数或数据成员的说明> //保护成员
private:
<成员函数或数据成员的说明> //私有成员
}; //花括号括住的属于类内范围
//类的实现部分
<各个成员函数的实现>
①class是声明类的关键字,类名是要声明的类的名字,必须符合标识符定义规则。
②花括号表示类的声明范围,其后的分号表示类声明结束。
③类的成员包括数据成员和成员函数,分别描述类所表达的属性和行为。
④关键字public、private和protected称为访问权限修饰符,它们限制了类成员的访问控制范围。
(3)类的数据成员:
①类中的数据成员描述类所表达的问题的属性。
②数据成员在类体中进行定义,其定义方式与一般变量相同,在定义类的数据成员时需要注意以下几个问题:
[1]对数据成员的访问会受到访问权限修饰符的控制。
[2]类中的数据成员可以是任意类型,包括整型、浮点型、字符型、数组、指针和引用等,也可以是其它类的对象(自身类不行,但自身类的指针可以)。
[3]在类体中不允许对所定义的数据成员进行初始化。
(4)类的成员函数:
①类的成员函数描述类所表达的问题的行为。
②各个成员函数的实现既可以在类体内定义(在类体内定义的成员函数都是内联函数,类体内定义则不需要对函数进行声明),也可以在类体外定义(类体外定义的话,类体内需要对函数进行声明)。
[1]如果一个成员函数在类体内进行了定义,它将不出现在类的实现部分。如果所有的成员函数都在类体内进行了定义,则可以省略类的实现部分。
[2]如果要将定义在类体外的成员函数也作为内联函数处理,就必须在成员函数的定义前加上关键字inline。
//类的说明部分
class <类名>
{
public:
<返回类型> <成员函数名>(<参数表>) //类内实现
{
<函数体>
}
<返回类型> <成员函数名>(<参数表>); //类内声明,类外实现
}; //花括号括住的属于类内范围
//类的实现部分
<返回类型> <类名>::<成员函数名>(<参数表>)
{
<函数体>
}
③成员函数除了可以定义为内联函数以外,也可以进行重载,还可以对其形参设置默认值。
2、对象的定义
(1)对象是类的实例,一个对象必须属于一个已知的类,因此在定义对象之前,必须先定义该对象所属的类。
(2)对象的定义格式如下:
<类名> <对象名>(<参数表>);
①类名是待定义的对象所属的类的名字。
②可以有一个或多个对象名,多个对象名之间用逗号分隔。
③参数表是初始化对象时需要的,建立对象时可以根据给定的参数调用相应的构造函数对对象进行初始化。无参数时表示调用类的默认构造函数。
(3)一个对象的成员就是该对象的类所定义的成员,包括数据成员和成员函数。定义了对象后,可以用“.”运算符和“->”运算符访问对象的成员,其中“.”运算符适用于一般对象和引用对象,而“->”运算符适用于指针对象(即指向对象的指针)。
<对象名>.<数据成员名> //通过对象名访问对象的数据成员
<指针名>-><数据成员名> //通过指向对象的指针访问对象的数据成员
<对象名>.<成员函数名>(<参数表>) //通过对象名访问对象的成员函数
<指针名>-><成员函数名>(<参数表>) //通过指向对象的指针访问对象的成员函数
3、举例
(1)例1:
#include<iostream>
using namespace std;
//设计一个圆类,求圆的周长(公式:C = 2 * PI * r)
const double PI = 3.14; //圆周率
class Circle //class代表设计一个类,类后面紧跟着的就是类名称
{
public: //公共权限
int m_r; //半径
double calculateZC() //行为:获取圆的周长
{
return 2 * PI * m_r;
}
};
int main() {
//通过圆类创建具体的圆(对象)
Circle c1;
//给圆对象的属性进行赋值
c1.m_r = 10;
cout << "圆的周长为:" << c1.calculateZC() << endl;
system("pause");
return 0;
}
(2)例2:
#include<iostream>
using namespace std;
#include<string>
//设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
class Student //class代表设计一个类,类后面紧跟着的就是类名称
{ //类中的属性和行为统一称为成员,属性又称为成员属性或成员变量,行为又称为成员函数或成员方法
public: //公共权限
string m_Name; //姓名
string m_Number; //学号
void show(); //行为:显示学生的信息(类外实现,类内声明)
void set(string name) //行为:获取学生的姓名(类内实现)
{
m_Name = name;
}
};
void Student::show()
{
cout << "姓名:" << m_Name << endl;
cout << "学号:" << m_Number << endl;
}
int main() {
//通过学生类创建具体的学生(实例化对象)
class Student S1;
//给学生对象的属性进行赋值
S1.set("姓名");
S1.m_Number = "学号";
S1.show();
class Student *S2 = &S1;
S2->set("xingming");
S2->m_Number = "xuehao";
S2->show();
system("pause");
return 0;
}
4、类成员的访问控制
(1)类中提供了公有(public)、私有(private)和保护(protected)三种访问控制权限。
①公有类型定义了类的外部接口,任何来自类外部的访问都必须通过外部接口进行。
②私有类型的成员只允许本类的成员函数访问,来自类外部的任何访问都是非法的(但是可以在类外调用成员函数间接访问类内的私有成员)。
③保护类型介于公有类型和私有类型之间,在继承和派生时可以体现出其特点。
(2)举例:
①例1:
#include<iostream>
using namespace std;
#include<string>
//三种权限
//公共权限 public 类内可以访问 类外可以访问
//保护权限 protected 类内可以访问 类外不可以访问 儿子也可以访问父亲的秘密
//私有权限 private 类内可以访问 类外不可以访问 儿子不能访问父亲的秘密
class Person //大括号内就是类内
{
public: //公共权限
string m_Name;
protected: //保护权限
string m_Car;
private: //私有权限
int m_Password;
public:
void func()
{
m_Name = "张三";
m_Car = "拖拉机";
m_Password = 1234;
}
};
int main() {
Person p1; //实例化具体对象
p1.m_Name = "李四";
//p1.m_Car = "奔驰"; 保护权限的内容在类外无法访问
p1.func(); //不过函数体可以访问类内,而函数体的访问权限是公共,类外可以访问函数体
system("pause");
return 0;
}
②例2:
#include<iostream>
using namespace std;
#include<string>
class Person //设计人类
{
public:
void setName(string name)
{
m_Name = name;
}
string getName()
{
return m_Name;
}
int getAge()
{
m_Age = 0; //初始化为0,这里只是为了m_Age有个值
return m_Age;
}
string setLover(string Lover)
{
m_Lover = Lover;
}
private:
string m_Name; //可读可写,写--设置 读--获取
int m_Age; //只读
string m_Lover; //只写
};
int main() {
Person p;
p.setName("姓名"); //写(设置)姓名
//p.m_Name = "姓名"; 不能直接写
cout << "姓名为:" << p.getName() << endl;
cout << "年龄为:" << p.getAge() << endl;
system("pause");
return 0;
}
③例3:
#include<iostream>
using namespace std;
//设计立方体类(Cube),求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等
class Cube
{
public:
void setL(int l) //设置长
{
m_L = l;
}
int getL() //获取长
{
return m_L;
}
void setW(int w) //设置宽
{
m_W = w;
}
int getW() //获取宽
{
return m_W;
}
void setH(int h) //设置高
{
m_H = h;
}
int getH() //获取高
{
return m_H;
}
int V(class Cube c1) //获取体积
{
return m_L * m_W * m_H;
}
int S(class Cube c1) //获取表面积
{
return 2*(m_L * m_W + m_L * m_H + m_H * m_W) ;
}
bool isSame(Cube c) //用成员函数判断是否相等
{
if (m_L == c.getL() && m_W == c.getW() && m_H == c.getH())
{
return true;
}
else
{
return false;
}
}
private:
int m_L;
int m_W;
int m_H;
};
//用全局函数判断是否相等
bool isSame(Cube &c1, Cube &c2) //引用可以节省内存
{
if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH())
{
return true;
}
else
{
return false;
}
}
int main() {
Cube c1;
Cube c2;
int l1, w1, h1, l2, w2, h2;
cout << "请依次输入c1的长、宽、高:" << endl;
cin >> l1;
cin >> w1;
cin >> h1;
cout << "请依次输入c2的长、宽、高:" << endl;
cin >> l2;
cin >> w2;
cin >> h2;
c1.setL(l1);
c2.setL(l2);
c1.setW(w1);
c2.setW(w2);
c1.setH(h1);
c2.setH(h2);
cout << "c1的表面积为:" << c1.S(c1) << " c2的体积为:" << c1.V(c1) << endl;
cout << "c2的表面积为:" << c2.S(c2) << " c2的体积为:" << c2.V(c2) << endl;
bool ret1 = isSame(c1, c2);
if (ret1 == true)
{
cout << "用全局函数判断是否相等:相等!" << endl;
}
else
{
cout << "用全局函数判断是否相等:不相等!" << endl;
}
bool ret2 = c1.isSame(c2);
if (ret2 == true)
{
cout << "用成员函数判断是否相等:相等!" << endl;
}
else
{
cout << "用成员函数判断是否相等:不相等!" << endl;
}
system("pause");
return 0;
}
④例4:
#include<iostream>
using namespace std;
#include<math.h>
//设计点类
class Point
{
public:
void setX(int x) //设置x
{
m_X = x;
}
int getX() //获取x
{
return m_X;
}
void setY(int y) //设置y
{
m_Y = y;
}
int getY() //获取y
{
return m_Y;
}
private:
int m_X;
int m_Y;
};
//设计圆类
class Circle
{
public:
void setR(int r) //设置半径
{
m_R = r;
}
int getR() //获取半径
{
return m_R;
}
void setCenter(Point center) //设置圆心
{
m_Center = center;
}
Point getCenter() //获取圆心
{
return m_Center;
}
private:
int m_R;
Point m_Center; //在类中可以让另一个类作为本类中的成员(注意先后顺序)
};
void isInCircle(Circle &c, Point &p)
{
int L2 = pow(c.getCenter().getX() - p.getX(), 2) + pow(c.getCenter().getY() - p.getY(), 2); //点到圆心的距离平方
int R2 = pow(c.getR(), 2); //圆半径平方
if (L2 == R2)
{
cout << "点在圆上" << endl;
}
else if (L2 > R2)
{
cout << "点在圆外" << endl;
}
else
{
cout << "点在圆内" << endl;
}
}
int main() {
Circle c1;
Point p1,p2;
int r, cx, cy, x, y;
cout << "依次输入圆的半径,圆心x坐标,圆心y坐标,点x坐标,点y坐标" << endl;
cin >> r;
cin >> cx;
cin >> cy;
cin >> x;
cin >> y;
c1.setR(r);
p2.setX(cx);
p2.setY(cy);
c1.setCenter(p2);
p1.setX(x);
p1.setY(y);
isInCircle(c1, p1);
system("pause");
return 0;
}
5、struct和class区别
(1)struct默认权限为公共,class默认权限为私有。
(2)结构体中没有成员函数的说法。
#include<iostream>
using namespace std;
class C1
{
int m_A; //默认是私有权限
};
struct C2
{
int m_A; //默认是公共权限
};
int main() {
C1 c1;
//c1.m_A = 10; 错误,访问权限是私有
C2 c2;
c2.m_A = 10; //正确,访问权限是公共
system("pause");
return 0;
}
三、构造函数和析构函数
1、概述
(1)定义一个类对象时,编译程序要为对象分配存储空间,进行必要的初始化,在C++中,这项工作是由构造函数来完成的,构造函数的作用是在对象被创建时利用特定的值构造对象,将对象初始化为一种特定的状态,使该对象具有区别于其它对象的特征。与构造函数对应的是析构函数,当撤销类对象时,析构函数负责回收存储空间,并做一些善后工作。
(2)构造函数也是类的成员函数,但它是一种特殊的成员函数,它除了具有一般成员函数的特性之外,还具有一些特殊的性质:
①构造函数的名字必须与类名相同。
②构造函数不指定返回类型,它隐含有返回值,由系统内部使用。
③构造函数可以有一个或多个参数(还可以设置默认参数),因此构造函数可以重载。
④在创建对象时,系统会自动调用构造函数。
(3)析构函数也是类中的一种特殊成员函数,它具有以下一些特性:
①析构函数名是在类名前加求反符号“~”。
②析构函数不指定返回类型,它不能有返回值。
③析构函数没有参数,因此析构函数不能重载,一个类中只能定义一个析构函数。
④在撤销对象时,系统会自动调用析构函数。
2、构造函数和析构函数的定义
(1)构造函数的定义(这里只展示类内定义):
class <类名>
{
public:
<类名>(<参数表>) : <数据成员名>(<表达式>), <数据成员名>(<表达式>), …
{
<构造函数体>
}
<成员函数或数据成员的说明>
};
①冒号后面是一个构造函数的初始化列表,用于初始化类中的各个数据成员。
②初始化列表位于构造函数的形参表之后,函数体代码之前,由一个冒号和逗号分隔的若干项构成。
③每一个构造函数的初始化列表项都由数据成员标识符和其后的括号表达式构成。
④在调用构造函数对类对象初始化时,先执行初始化列表对各个成员进行初始化,再执行构造函数体。
⑤初始化列表中各个初始化项的执行顺序取决于类成员在类中声明的顺序,而与初始化列表给出的初始化项的顺序无关。
⑥对于大多数数据成员而言,既可以使用初始化列表的方式获得显式初值,也可以在获得默认初值后再在构造函数体中使用赋值语句将表达式的值赋值给数据成员。
⑦当一个类的成员是另外一个类的对象时,该对象就称为成员对象,当类中出现了成员对象时,该类的构造函数要包含成员对象的初始化。
(2)析构函数的定义:
class <类名>
{
public:
~<类名>( )
{
<析构函数体>
}
<成员函数或数据成员的说明>
};
(3)举例:
#include<iostream>
using namespace std;
class Person
{
public:
//构造函数:没有返回值,也不写void;与类名相同;可以有参数,可以发生重载,可以设置默认参数;创建对象时会自动调用,且只调用一次
Person(int _age, int _height, bool _sex = false) : age(_age), sex(_sex)
{
cout << "Person构造函数的调用" << endl;
height = _height;
}
//析构函数:没有返回值,也不写void;与类名相同,在名前加“~”;不可以有参数,不可以发生重载;对象在销毁前会自动调用析构函数,且只调用一次
~Person()
{
cout << "~Person析构函数的调用" << endl;
system("pause"); //这里安排停顿只是为了方便捕捉主函数结束后调用的析构函数
}
int age;
int height;
bool sex;
};
void test01()
{
Person p(28, 170); //局部变量,在栈上的数据,test01执行完毕后就会释放(销毁)这个对象
//调用对象p的构造函数
return;
//调用对象p的析构函数
}
int main() {
test01(); //在子函数中创建的对象,在子函数运行完成后会被销毁
Person p(18, 160, true); //在主函数中创建的对象,主函数执行完return 0一步才会对对象进行销毁
//调用对象p的构造函数
//system("pause");
return 0;
//调用对象p的析构函数
}
3、默认构造函数和默认析构函数
(1)构造和析构都是必须有的实现,否则编译器会自己提供空实现。
(2)构造函数的空实现:
class <类名>
{
public:
<类名>( )
{
}
};
(3)析构函数的空实现:
class <类名>
{
public:
~<类名>( )
{
}
};
4、拷贝构造函数
(1)类中有一种特殊的构造函数叫做拷贝构造函数(也叫复制构造函数),它用一个已知的对象初始化一个正在创建的同类对象。
(2)拷贝构造函数的一般格式如下(这里仅给出类内定义的构造函数):
class <类名>
{
public:
<类名>(const <类名>& <引用对象名>)
{
<拷贝构造函数体>
}
<成员函数或数据成员的说明>
};
(3)拷贝构造函数具有以下特点:
①拷贝构造函数也是一种构造函数,因此函数名与类名相同,并且不能指定函数返回类型。
②只有一个参数,是对同类的某个对象的引用。
③每一个类中都必须有一个拷贝构造函数,如果类中没有定义拷贝构造函数,编译器会自动生成一个具有上述形式的公有拷贝构造函数。
(4)拷贝构造函数在以下三种情况下会被调用:
①用类的一个已知的对象去初始化该类的另一个正在创建的对象。
②采用值传递调用方式时,对象作为函数实参传递给函数形参。
③对象作为函数返回值。
#include<iostream>
using namespace std;
/*C++中拷贝构造函数调用时机通常有三种情况:
①使用一个已经创建完毕的对象来初始化一个新对象
②值传递的方式给函数参数传值
③以值方式返回局部对象*/
class Person
{
public:
Person()
{
cout << "Person无参构造函数的调用" << endl;
}
Person(int a)
{
age = a;
cout << "Person有参构造函数的调用" << endl;
}
Person( const Person &p )
{
age = p.age; //将传入的人身上的所有属性拷贝到这里
cout << "Person拷贝构造函数的调用" << endl;
}
~Person() //析构函数
{
cout << "Person析构函数的调用" << endl;
}
int age;
};
void test01() //使用一个已经创建完毕的对象来初始化一个新对象
{
cout << "test01:" << endl;
Person p1(20);
Person p2(p1);
cout << "p2的年龄为:" << p1.age << endl;
cout << "p3的年龄为:" << p2.age << endl;
}
void doWork(Person p)
{
;
}
void test02() //值传递的方式给函数参数传值
{
cout << "test02:" << endl;
Person p;
doWork(p); //实参传给形参时,形参自己创建了一个对象,调用拷贝构造函数
}
Person doWork2()
{
Person p1;
cout << (int*)&p1 << endl;
return p1;
}
void test03() //以值方式返回局部对象
{
cout << "test03:" << endl;
Person p;
p = doWork2(); //函数返回时会创建一个新的对象,拷贝一份p1返回来,调用拷贝构造函数
cout << (int*)&p << endl;
}
int main() {
test01();
test02();
test03();
system("pause");
return 0;
}
5、构造函数的分类及调用
(1)根据参数列表是否为空可将构造函数分为有参构造和无参构造,根据参数类型可将构造函数分为普通构造和拷贝构造。
(2)调用构造函数的三种方式:括号法、显示法、隐式转换法。
#include<iostream>
using namespace std;
class Person
{
public:
Person() //构造函数
{
cout << "Person无参构造函数的调用" << endl;
}
Person(int a) //构造函数
{
age = a;
cout << "Person有参构造函数的调用" << endl;
}
Person( const Person &p ) //拷贝构造函数
{
age = p.age; //将传入的人身上的所有属性拷贝到这里
cout << "Person拷贝构造函数的调用" << endl;
}
~Person() //析构函数
{
cout << "Person析构函数的调用" << endl;
}
int age;
};
void test01()
{
//括号法(推荐)
Person p1; //默认构造函数调用(千万不要加小括号,否则编译器会认为是函数声明)
Person p2(10); //有参构造函数调用
Person p3(p2); //拷贝构造函数调用
cout << "p2的年龄为:" << p2.age << endl;
cout << "p3的年龄为:" << p3.age << endl;
//显示法
Person p4; //默认构造函数调用
Person p5 = Person(10); //有参构造函数调用
Person p6 = Person(p5); //拷贝构造函数调用(不要利用拷贝函数初始化匿名对象,即Person(p5);)
Person(10); //匿名对象,创建了一个没名字的对象,当前行执行结束后系统会立即回收它
cout << "看看Person(10)啥时候被销毁" << endl;
//隐式转换法
Person p7 = 10; //相当于Person p7 = Person(10); 有参构造函数调用
Person p8 = p7; //相当于Person p2 = Person(p7); 拷贝构造函数调用
}
int main() {
test01();
system("pause");
return 0;
}
(3)默认情况下,C++编译器至少会给一个类添加3个函数,分别为默认构造函数(无参,函数体为空)、默认析构函数(无参,函数体为空)、默认拷贝构造函数。
①如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造。
②如果用户定义有拷贝构造函数,C++不会再提供其它构造函数。
#include<iostream>
using namespace std;
class Person {
public:
//无参(默认)构造函数
Person()
{
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a)
{
age = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数(如果用户定义拷贝构造函数,c++不会再提供其他构造函数)
/*Person(const Person& p)
{
age = p.age;
cout << "拷贝构造函数!" << endl;
}*/
//析构函数
~Person()
{
cout << "析构函数!" << endl;
}
public:
int age;
};
void test01()
{
cout << "test01:" << endl;
Person p1(20);
Person p2(p1);
cout << "p1的年龄为:" << p1.age << endl;
cout << "p2的年龄为:" << p2.age << endl;
}
void test02()
{
cout << "test02:" << endl;
Person p; //如果只有有参构造函数而没有无参构造函数,编译器不再提供默认构造函数,那么这行代码是会报错的
}
int main() {
test01();
test02();
system("pause");
return 0;
}
(4)类对象作为类成员,定义类对象时,先调用类成员的构造函数,再调用类对象的构造函数;销毁类对象时,先调用类对象的析构函数,再调用类成员的析构函数。(类中有多个成员对象时,成员对象构造函数的执行顺序仅与成员对象在类中声明的顺序有关,而与成员初始化列表中给出的成员对象的顺序无关)
#include<iostream>
using namespace std;
#include<string>
//类对象作为类成员
class Phone
{
public:
Phone(string pname)
{
m_pname = pname;
cout << "Phone的构造函数调用" << endl; //先构造类下的对象
}
~Phone()
{
cout << "Phone的析构函数调用" << endl;
}
string m_pname;
};
class Person
{
public:
Person(string name, string pname):m_name(name),m_phone(pname) //相当于隐式转换法Phone m_phone = Phone(pname);
{
cout << "Person的构造函数调用" << endl; //再构造本类
}
~Person()
{
cout << "Person的析构函数调用" << endl; //析构的顺序与构造相反
}
string m_name;
Phone m_phone;
};
void test01()
{
Person p("张三", "华为");
cout << p.m_name << "拿着" << p.m_phone.m_pname << endl;
};
int main() {
test01();
system("pause");
return 0;
}