写在前面
写本系列的目的(自用)是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。以前三天打鱼两天晒网地学习C++,一直无法对C++熟练掌握,核心的面向对象编程并不熟悉,STL语法只会部分,因此也希望自己未来能做到熟悉甚至精通C++。
Part 2 面向对象编程
该部分主要通过b站黑马程序员的视频来进行学习,记录笔记。
一、函数重载
在介绍函数重载前需要补充两个前置知识,函数的默认参数及函数的占位参数。
1、函数的默认参数
函数的默认参数在前面函数部分有简单提到过,在C++中,函数的形参列表可以有默认值。
int func(int a, int b = 10) //这里的int b = 10就是函数的默认参数
{
return a+b;
}
int main()
{
int a = 5, b = 20;
int c = func(a); //仅传入一个参数时,剩下的参数选用默认参数,c = 5 + 10 = 15
int d = func(a, b); //传入两个参数,则以传入的参数为主,d = 5 + 20 = 25
cout<< c << " "<< d <<end; //可以验证一下
return 0;
}
2、函数的占位参数
C++的形参列表可以有占位参数,调用函数时必须填补该位置。
void func(int a, int) //这里单独的那个int就是占位参数,只写数据类型
{
cout<< "func" << endl;
}
void func2(int a, int = 10) //此外,占位参数也可以有默认参数
{
cout<< "func2" <<end;
}
int main()
{
fun(10, 10); //占位参数必须填补,函数才能正常调用
//占位参数就好比在自习室,需要去卫生间时,放个书包在位置上占位
//只不过C++里放置的是参数类型
func2(10); //这是有默认参数的情况
return 0;
}
3、函数重载
函数重载需要满足以下三个条件:
1、在同一个作用域下
2、函数名称相同
3、函数参数 类型不同 或 个数不同 或 顺序不同
注:函数的返回值不能作为函数重载的条件
函数重载使得函数名可以相同,提高函数的复用性。
//目前的代码都在全局作用域中,涉及到类后会出现其他作用域
void func() //函数重载其实只需要你从编译器的角度去考虑
{
cout<< "func()"<<endl;
}
/*
void func() //例如此处我注释的这段代码,如果解开注释,运行时就会出错
{ //两个完全相同的函数并不能满足函数重载
cout<< "func()!"<<end; //在调用func()时,编译器不知道调哪个函数,哪怕函数体内不同也不行
}
*/
void func(int a) //这个函数(第二个函数)和第一个函数的参数个数不同
{
cout<< "func(int)"<<endl;
}
void func(double a) //第三个和第二个函数类型不同
{
cout<< "func(double)"<<endl;
}
void func(int a, double b) //以下是4,5两个函数,两个函数参数顺序不同
{
cout<< "func(int, double)"<<endl;
}
void func(double b, int a)
{
cout<< "func(double, int)"<<endl;
}
/*
int func(double b, int a) //注,注释内的函数和第五个函数仅是返回值不同
{ //此时也不满足函数重载条件
cout<< "func(double, int)"<<endl; //从编译器的角度考虑,调用函数时使用func(3.14, 10);
} //无法从返回值的角度区分
*/
int main()
{
func(); //会调用第一个函数
func(10); //会调用第二个函数
func(3.14); //会调用第三个函数
func(10, 3.14); //会调用第四个函数
func(3.14, 10); //会调用第五个函数
return 0;
}
其实在介绍完函数重载后,就知道占位参数的作用了,占位参数可以在函数重载中使用,保证函数参数不同等。此外,还需要补充函数重载和引用、函数重载和默认参数
//函数重载和引用
void func(int &a) //这两个函数可以满足函数重载
{ //加上const前后是两个不同类型
cout<< "func(int &a)"<<endl;
}
void func(const int &a)
{
cout<< "func(const int &a)"<<endl;
}
int main()
{
int a = 10;
func(a); //会调用第一个函数
//对于这个函数,需要注意的是不能传入func(10), 传入会变为int &a = 10, 所以需要传入变量a
func(10); //会调用第二个函数,可以将传入参数带入来进行理解
//传入10时,对于第一个函数而言,时int &a = 10,我们知道引用是别名,之前介绍过引用=常数,不合法
//对于第二个函数, const int &a = 10 合法
return 0;
}
//函数重载和默认参数
void func(int a, int b = 10)
{
cout<< "func(int a, int b = 10)"<<endl;
}
void func(int a)
{
cout<< "func(int a)"<<endl;
}
int main()
{
func(10); //注意,此时会报错,从编译器的角度来考虑。对于函数1,传入func(10)可以运行
return 0; //此时会使用b的默认值。对于函数2,原本就只需要传入一个参数,也可以运行
} //因此编译器区分不了,会报错
二、类和对象
现在就正式进入面向对象编程了,这是C++中最重要的内容!
C++面向对象的三大特性为:封装、继承、多态
C++中万事万物都皆为对象,且对象上有其属性和行为
例如:人可以作为一个具体的对象,人的属性可以有姓名、年龄、身高、体重等,行为可以有跑、跳等。在游戏开发过程中就经常使用类来表示人,包括姓名、职业、攻击方式,受击反馈等。
而具有相同性质的对象,就可以抽象为类,人属于人类,车属于车类。具体的对象就是类的实例化。
1、封装
封装的意义:
1、将属性和行为作为一个整体,表现生活中的事物
2、将属性和行为加以权限控制
此外,需要知道的是,类中的属性和行为,我们统一称之为成员;
属性又可称为 成员属性 和 成员变量 ;行为又可称之为 成员函数 和 成员方法。
(1)属性和行为作为一个整体
以下是一个简单的类的示例,主要体现封装的第一种意义
const double PI = 3.14;
class Circle //这是一个圆类,可以看到,圆的属性和行为都封装在这个类里
{
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;
}
同样的,在类中,除了像上述例子一样为其属性赋值,还可以通过行为为其赋值
const double PI = 3.14;
class Circle
{
public: //代表是公共权限,关于权限的内容后续会介绍
int m_r;
void setR(int r) //为圆的半径赋值
{
m_r = r;
}
double calculateZC() //打印圆的周长
{
return 2 * PI * m_r;
}
};
int main()
{
Circle c1; //通过圆类创建一个具体对象,又叫实例化
c1.setR(10); //为其属性赋值
cout<< "周长为" << c1.calculateZC() <<endl; //调用类行为
system("pause");
return 0;
}
(2)权限控制
下面介绍封装的第二种意义,权限控制
类在设计时,可以把属性和行为放在不同的权限下,加以控制。访问权限有三种:
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 = 123456; //可以正常访问
}
};
int main()
{
Person p1;
p1.m_Name = "李四"; //可以正常执行并修改
//p1.m_Car = "奔驰"; //这是保护权限内容,在类外不能访问
//p1.m_Password = 1234; //这是私有权限内容,在类外不能访问
p1.func(); //可以正常调用,因为调用的函数属于公共部分,借助公共部分的函数
//在类的内部访问类的私有及保护部分
}
在这里可以谈一下struct和class的区别了。它们唯一的区别在于访问权限不同。struct默认为公共,class默认为私有。这也是在以上代码中经常会写public的原因。
(3)将成员属性设置为私有
将成员属性设置为私有,可以巧妙地控制类内成员的读写权限。
class Person
{
public:
void setName(string name) //写姓名部分
{
m_Name = name;
}
string getName() //读姓名部分
{
return m_Name;
}
string getAge() //读年龄部分,由于年龄权限为只读,因此只有一个函数
{
return m_Age;
}
void setIdol(string idol) //写偶像部分,由于偶像权限为只写,因此只有一个函数
{
m_Idol = idol;
}
private: //均设置为私有部分,通过公共部分的函数来巧妙实现权限控制
string m_Name; //可读可写
int m_Age = 18; //只读
string m_Idol; //只写
}
int main()
{
Person p;
p.setName("张三");
cout << "姓名是:" << p.getName() <<endl;
cout << "年龄是:" << p.getAge() <<endl;
p.setIdol("小明");
return 0;
}
2、对象特性
C++中的面向对象来源于生活,每个对象也都会有 初始设置 以及 对象销毁前的清理数据的设置。
一个对象或者变量没有初始状态,对其使用后果是未知。同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题。
(1)构造函数和析构函数
C++使用构造函数和析构函数来分别进行对象的初始化和对象的清理。如果用户没有编写这两个函数,那么这两个函数都是由系统默认实现,只是系统实现的函数是空实现:
class Example
{
public:
Example(){ } //构造函数
~Example(){ } //析构函数
}
构造函数:创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。语法为:
1、没有返回值也不写void
2、函数名称与类名相同
3、构造函数可以有参数,因此可以发生重载
4、程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数:对象销毁前系统自动调用,执行一些清理工作。语法为:
1、没有返回值也不写void
2、函数名称与类名相同,在名称前加上符号 ~
3、析构函数不可以有参数,因此不可以发生重载
4、程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
下面是自己编写析构函数时,系统调用用户编写的函数
class Person
{
public:
Person() //构造函数
{
cout<<"构造函数"<<endl;
}
~Person() //析构函数
{
cout<<"析构函数"<<endl;
}
};
void test()
{
Person p; //该函数中创建一个局部变量,在函数调用开始时创建,函数调用
} //结束时销毁
int main()
{
test(); //因此在执行这条语句时析构函数和构造函数都会调用,输出两句
Person p1; //该变量会在main函数结束时销毁
//因此此时调用只会看到构造函数
system("pause"); //在界面中按下“enter”后能在窗口关闭的一瞬间看到析构函数
return 0;
}
(2)构造函数的分类及调用
1、构造函数有两种分类方式:
按参数分为: 有参构造和无参构造
按类型分为: 普通构造和拷贝构造
2、构造函数有三种调用方式:括号法、显示法、隐式转换法
这里说一下我对构造函数的理解:构造函数就是你选用的一种初始化你的对象的统一方式。
在默认情况下系统进行调用空构造,此时不对你的对象来进行统一初始化。
如果采用有参构造,那么就是你指定用某参数来对创建的对象的某属性进行初始化。
如果采用拷贝构造,那么就是你希望新的对象能够和某个已有对象属性相同,以此来进行拷贝初始化。
下面看案例:
class Person
{
private:
int age;
public:
Person() //无参构造函数,又称默认构造,也是普通构造函数
{
cout<<"无参构造函数"<<endl;
}
Person(int a) //有参构造函数,也是普通构造函数
{
age = a; //这里给属性age赋值
cout<<"有参构造函数"<<endl;
}
Person(const Person &p) //拷贝构造函数,也是有参构造函数
{
age = p.age; //这里将已有对象p赋值给新对象
cout<<"拷贝构造函数"<<endl; //要注意必须用const,防止对已有对象进行修改
}
~Person() //析构函数
{
cout<<"析构函数"<<endl;
}
};
void test()
{
//括号法
Person p1; //默认构造函数调用,会调用第一个构造函数
//此外,需要注意的是,调用默认构造函数,不要加()
//因为若写成Person p1(); 编译器会认为这是一个函数声明,而非创建对象
Person p2(10); //有参构造函数调用,会调用第二个构造函数
Person p3(p2); //拷贝构造函数调用,会调用第三个构造函数
cout<<"p2的年龄是:"<<p2.age<<endl; //在这里进行验证,可以发现p2的年龄是10
cout<<"p3的年龄是:"<<p3.age<<endl; //p3是p2的拷贝,p3的年龄也是10
//显示法
Person p4; //默认函数构造
Person p5 = Person(10); //有参构造
Person p6 = Person(p5); //拷贝构造
//显示法会让人想到 int a = add(10, 5); 这样的语法,这里假设你已经书写了add函数
//我们也知道,如果单独写add(10, 5), add函数的返回值没有变量接收,系统会立即回收这部分内存
Person(10); //那么这么写也同理,这是一个匿名对象,当这行执行完后
//系统会立即回收匿名对象的内存空间
//Person(p3); //需要补充的是,不要用拷贝函数写匿名对象
//编译器会认为 Person(p3) 等价于 Person p3
//隐式转换法
Person p7 = 10; //相当于Person p7 = Person(10),有参构造
Person p8 = p7; //相当于Person p8 = Person(p7),拷贝构造
}
int main()
{
test();
system("pause");
return 0;
}
篇幅问题接下来的内容写在下一篇笔记中。