C++学习2
在上一节的学习中,主要还是在复习C语言的特性,包括最后的实战篇,也没用上面向对象的设计思想。但从这一节开始,正式开始学习属于C++的特性,并且学习面向对象的编程思想。
注:本文是参考黑马程序员的C++视频教程后自己总结的,添加了一部分自己的东西,属于二次创作,全投转载也太那啥了,我干脆一个投转载一个投原创算了,谁让CSDN没有二次创作的选项呢(任性)。建议想要学习C++的小伙伴每人都自己写一篇总结,加深理解。
这是本人的学习记录,通篇又臭又长,不建议观看,有作业需求的自取即可
面向对象语法
new操作符
在C++中使用new关键字申请堆中内存
堆区申请的内存不会被系统自动释放,由程序员使用delete关键字手动释放
语法:new 数据类型
利用new创建的数据,会返回该数据对应类型的指针
示例:
#include <iostream>
#include <string>
using namespace std;
int main()
{
/*在堆区申请一个整形*/
int* p = new int(10); //圆括号内的数据是为这块内存赋值用的
cout << *p << endl; //输出为10
delete p; //释放内存
/*在堆区申请一个数组*/
int* p1 = new int[10]; //方括号代表申请的是一个数组,方括号内数字代表数组元素个数
for (int i = 0; i < 10; i++) //为数组赋值
{
p1[i] = i;
}
for (int i = 0; i < 10; i++) //打印数组,输出0到9
{
cout << p1[i] << endl;
}
delete[] p1; //释放数组的时候要加一个方括号,告诉编译器释放的是一个数组
system("pause");
return 0;
}
总结:
- 相比malloc,new可以在申请内存的同时给内存赋值,值放在圆括号内。这不光能为整形、浮点型赋值,也可以为string赋值(括号里加上双引号代表它是个字符串),甚至可以为自定义的结构体赋值(括号里需要加上大括号,如:
student* p = new student({ 12, "张三" });
) - 在释放数组的时候,不要忘了在delete后面加上一对方括号。
引用
引用基本语法
作用:给变量起别名
语法:数据类型 &别名 = 原名;
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
int main()
{
int a = 10; //定义a等于10
int& b = a; //b引用a
cout << a << endl; //a等于10
cout << b << endl; //b等于10
b = 100;
cout << a << endl; //a等于100
cout << b << endl; //b等于100
system("pause");
return 0;
}
引用注意事项
- 引用必须初始化
- 引用在初始化后不可以改变
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
int main()
{
int a = 10; //定义a等于10
/*1、引用必须初始化*/
//int& b; //错误语法
int& b = a;
/*2、引用在初始化后不可改变*/
int c = 20;
//&b = c; //错误语法
b = c; //语法没错,但这是赋值操作
cout << a << endl; //三者输出都为20
cout << b << endl;
cout << c << endl;
system("pause");
return 0;
}
引用做函数参数
作用:函数传参时,可以利用引用来使形参修改实参
优点:简化指针的操作,并节省形参中指针所占内存
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
void mySwap(int& a, int& b);
int main()
{
int a = 10;
int b = 20;
cout << "a = " << a << endl; //打印交换前a、b的值
cout << "b = " << b << endl;
mySwap(a, b); //交换a、b
cout << "a = " << a << endl; //打印交换后a、b的值
cout << "b = " << b << endl;
system("pause");
return 0;
}
void mySwap(int& a, int& b) //引用传参
{
int temp = a;
a = b;
b = temp;
}
引用做函数返回值
作用:引用可以修改函数内部的静态局部变量
注意:不要返回局部变量的引用
用法:函数调用为左值
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
int& test1();
int& test2();
int main()
{
/*引用局部变量的案例*/
int& ref1 = test1();
cout << "ref1 = " << ref1 << endl; //第一次输出仍然是10,是因为栈的内存还未被改写,实际已是非法操作
cout << "ref1 = " << ref1 << endl; //二三两次输出的就不再是10
cout << "ref1 = " << ref1 << endl;
/*引用静态局部变量*/
int& ref2 = test2();
cout << "ref2 = " << ref2 << endl; //静态局部变量的内存不会被自动释放,所以不去人为修改就不会变
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
/*函数调用作左值*/
test2() = 1000; //函数返回的是静态变量a的引用,所以这句语句会为a赋值
cout << "ref2 = " << ref2 << endl; //输出1000
system("pause");
return 0;
}
int& test1()
{
int a = 10;
return a;
}
int& test2()
{
static int a = 10;
return a;
}
引用的本质
本质:引用的本质在C++内部实现是一个指针常量
示例:
/*发现是引用,转化为 int* const ref = &a */
void func(int& ref)
{
ref = 100; //内部发现ref是引用,转化为: *ref = 100;
}
int main()
{
int a = 10;
/*自动转化为 int* const ref = &a; */
int& ref = a;
ref = 20; //内部发现ref是引用,自动转化为: *ref = 20;
func(a);
return 0;
}
结论:推荐使用引用,因为语法方便。引用的本质是指针常量,但内部的实现编译器已经帮我们做了,不需要去深究
个人理解:
- 引用和变量占用相同的内存,好似变量的影子
- 引用的值就是变量的值,引用就是变量的本身,只是换了个名字
优点:
- 相比于值传递,不但节省形参的内存,还可以通过形参修改实参
- 相比于地址传递,可以节省形参中指针的内存
- 可以使用引用修改函数内部的静态局部变量
常量引用
作用:常量引用主要用来修饰形参,防止误操作修改了实参
在函数形参列表中,可以加const修饰形参,防止形参修改实参
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
void showNum(const int& a);
int main()
{
/*引用常量*/
//int& ref = 10; 语法错误,引用必须引用一块合法的内存
const int& ref = 10; //合法操作,引用加const修饰可以引用常量
/*常量引用作形参*/
int a = 10;
showNum(a);
system("pause");
return 0;
}
void showNum(const int& a)
{
//a = 100; 语法错误,常量不可修改
cout << a << endl;
}
函数提高
函数的默认参数
在C++中,函数的形参可以有默认值
语法:返回值类型 函数名(参数 = 默认值);
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
int test1(int a, int b = 20, int c = 30); //函数声明含有默认参数
int main()
{
cout << test1(10) <<endl; //输出60:a=10;b=10;c=30
cout << test1(10, 10) << endl; //输出50:a=10;b=10;c=30
cout << test1(10, 10, 10) << endl; //输出30:a=10;b=10;c=10
system("pause");
return 0;
}
int test1(int a, int b, int c) //函数声明含有默认参数,函数的定义就不能含有默认参数
{
return a + b + c;
}
注意事项:
- 一旦函数的某个形参使用了默认参数,那么这个形参右边的所有形参都必须含有默认参数。像
int test1(int a, int b = 20, int c);
这样的是不允许的。 - 如果函数的声明含有默认参数,那么这个函数的定义中就不能含有默认参数。不然一旦函数声明和定义采用了不一样的默认参数,编译器就不知道该采用哪个默认参数。同样的,多次函数声明也只有一个声明能含有默认参数(在某个函数内部作函数声明可以赋予不同的默认参数,但不要这样做!!!)。
函数的占位参数
C++中函数的形参列表里可以有占位参数,用来做占位。调用函数时必须要填补该位置
语法:返回值类型 函数名(数据类型);
现阶段占位参数的意义不大,先做了解,后续会真正用到
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
void test1(int a, int);
void test2(int a, int = 100);
int main()
{
test1(10,100); //输出a = 10。但后面的100在函数中用不到
test2(10); //占位参数还可以含有默认参数
system("pause");
return 0;
}
void test1(int a, int)
{
cout << "a = " << a << endl;
}
void test2(int a, int)
{
cout << "a = " << a << endl;
}
函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名相同
- 函数参数类型不同或个数不同或顺序不同
注意:函数的返回值不同不可以作为函数重载的条件
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
struct Point
{
int x;
int y;
};
Point point;
void setPoint(int x, int y);
void setPoint(const Point* p);
int main()
{
setPoint(10, 20);
cout << point.x << "," << point.y << endl;
Point p;
p.x = 20;
p.y = 30;
setPoint(&p);
cout << point.x << "," << point.y << endl;
system("pause");
return 0;
}
void setPoint(int x, int y)
{
point.x = x;
point.y = y;
}
void setPoint(const Point* p)
{
point.x = p->x;
point.y = p->y;
}
总结:
函数重载使得函数名的复用性得到了提高,用户可以使用同一个函数名去调用不同的函数。像本例中,用户想要改变结构体point成员的值,可以使用两种方法,提高了自由度。但是,下面这两个函数是不可以被重载的:
void setPoint(int x);
void setPoint(int y);
注意:
函数的重载虽然为我们提供了一定的便利,但同时也带来了很多困扰,在设计程序时一定要极力避免
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
void test1(int a);
void test1(short a);
int main()
{
auto a = 10;
int b = 10;
short c = 10;
char d = 10;
test1(10);
test1(a);
test1(b);
test1(c);
test1(d);
system("pause");
return 0;
}
void test1(int a)
{
cout << "我是int" << endl;
}
void test1(short a)
{
cout << "我是short" << endl;
}
试问:如果看到了这样的代码,会不会想打人?
如果还觉得不够过瘾,那么这样呢?
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
void test1(int a, short b);
void test1(short a, int b);
int main()
{
unsigned int a = 10;
int b = 10;
short c = 10;
char d = 10;
test1(a, b);
test1(c, d);
test1(d, c);
system("pause");
return 0;
}
void test1(int a, short b)
{
cout << "我是int" << endl;
}
void test1(short a, int b)
{
cout << "我是short" << endl;
}
函数重载注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数
示例1:函数重载碰到引用
void test(int a);
void test(int& a);
void test(const int& a);
int main()
{
int a = 10;
test(a);
return 0;
}
示例2:函数重载碰到默认参数
void test(int a);
void test(int a, int b = 10);
int main()
{
int a = 10;
test(a);
return 0;
}
总结:
在使用函数重载时,一定要避免出现二义性。
类和对象
C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物皆为对象,对象上有其属性和行为
例:
人可以作为对象,属性有姓名、身高、体重等,行为有走、跑、跳、吃饭唱歌等
车也可以作为对象
封装
封装的意义
封装是C++面向对象三大特性之一
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装的意义一:属性加行为
在设计类的时候,属性和行为写在一起,表现事物
语法:class 类名{ 访问权限: 属性 / 行为 };
示例1:设计一个圆类,求圆的周长
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
#define 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;
}
示例2:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Student //学生类
{
public: //公共权限
/*属性*/
string m_name; //姓名
int m_num; //学号
/*行为*/
void setName(string name)//设置姓名
{
m_name = name;
}
void setNum(int num) //设置学号
{
m_num = num;
}
string getName() //获取姓名
{
return m_name;
}
int getNum() //获取学号
{
return m_num;
}
};
int main()
{
/*创建一个学生对象*/
Student s1;
/*为它赋值*/
s1.setName("张三");
s1.setNum(18);
/*获取它的值并打印*/
cout << s1.getName() << endl;
cout << s1.getNum() << endl;
system("pause");
return 0;
}
封装的意义二:权限
类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
- public 公共权限
- protected 保护权限
- private 私有权限
解释一下它们的含义:
- 公共权限:类内可以访问,类外也可以访问
- 保护权限:类内可以访问,类外不可以访问,但可以被子对象继承
- 私有权限:类内可以访问,类外不可以访问,且无法被子对象继承
注:类外访问也就是在类外的函数调用这样的语句:对象.属性 = xxx;
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person //人类
{
public: //公共权限
string m_name; //姓名
protected: //保护权限
string m_car; //汽车
private: //私有权限
int m_password; //密码
protected: //方法同样可以设置权限
void show1() //保护权的限方法
{
cout << m_name << endl;
cout << m_car << endl;
cout << m_password << endl;
}
public: //公共权限的方法
void init()
{
m_name = "张三"; //这就是类内访问
m_car = "拖拉机";
m_password = 123456;
}
void show2()
{
show1(); //无论什么权限,在类内都可以被访问、调用
}
};
int main()
{
/*创建一个人对象*/
Person p1;
p1.init(); //公共权限的方法,可以在类外被调用
/*这就是类外访问,无论是读还是写*/
p1.m_name = "李四"; //合法操作,因为它是公共权限
//p1.m_car = "奔驰"; //语法错误,保护权限无法在类外访问
//p1.m_password = 123; //语法错误,私有权限无法在类外访问
/*打印*/
cout << p1.m_name << endl;
//cout << p1.m_car << endl; //这两句同样会报错
//cout << p1.m_password << endl;
/*调用两个不同权限的方法*/
//p1.show1(); //编译器同样会报错
p1.show2();
system("pause");
return 0;
}
struct和class的区别
在C++中,struct和class唯一的区别就是默认的访问权限不同:
- struct默认全部为公共权限
- class默认全部为私有权限
struct C1
{
int m_a; //默认为公共权限
void init(int a) //下面两个方法也默认是公共权限
{
m_a = a;
}
void show()
{
cout << m_a << endl;
}
};
class C2
{
int m_a; //默认为私有权限
void init(int a) //下面两个方法也默认是私有权限
{
m_a = a;
}
void show()
{
cout << m_a << endl;
}
};
总结:
在C++中,结构体同样可以包含方法,可以添加不同的权限,除了默认权限不同,其他都一样。
而在C语言中,结构体就没有这么强大了,所有的成员都只能是公共权限,且结构体中不能含有方法,只能通过函数指针来部分实现。
成员属性设为私有
优点:
- 将所有成员属性设为私有,可以自己控制读写权限
- 对于写权限,可以在方法中添加断言来检测输入数据有效性、合法性
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person
{
public:
void setName(string name) //设置姓名 写
{
m_name = name;
}
string getName() //获取姓名 读
{
return m_name;
}
int getAge() //获取年龄 只读
{
m_age = 10;
return m_age;
}
void setPassword(int password)//设置密码 只写
{
/*只允许设置5位数密码,设置错误自动还原成0*/
if (password < 10000 || password>99999)
{
cout << "请设置5位数密码" << endl;
m_password = 0;
return;
}
m_password = password;
}
private:
string m_name; //姓名 可读可写
int m_age; //年龄 只读
int m_password; //密码 只写
};
int main()
{
Person p1;
/*姓名为可读可写*/
p1.setName("张三");
cout << p1.getName() << endl;
/*年龄为只读*/
cout << p1.getAge() << endl;
/*密码为只写*/
p1.setPassword(12345);
system("pause");
return 0;
}
练习案例1:设计立方体类
设计立方体类(Cube)
求出立方体的面积和体积
分别用全局函数和成员函数判断两个立方体是否相等
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Cube
{
public:
/*设置长宽高*/
void setSize(int l, int w, int h)
{
m_length = l;
m_width = w;
m_height = h;
}
/*获取长宽高*/
int getLen()
{
return m_length;
}
int getWid()
{
return m_width;
}
int getHei()
{
return m_height;
}
/*计算周长*/
int calculateS()
{
return 2 * (m_length * m_width + m_width * m_height + m_height * m_length);
}
/*计算面积*/
int calculateV()
{
return m_length * m_width * m_height;
}
/*判断两个立方体是否相等*/
bool judgeCube(Cube& cube)
{
int temp[2][3] =
{
{m_length, m_width, m_height },
{cube.getLen(), cube.getWid(), cube.getHei()}
};
for (int i = 0; i < 3 - 1; i++)
{
for (int j = 0; j < 3 - 1 - i; j++)
{
if (temp[0][j] > temp[0][j + 1])
{
int buf = temp[0][j];
temp[0][j] = temp[0][j + 1];
temp[0][j + 1] = buf;
}
if (temp[1][j] > temp[1][j + 1])
{
int buf = temp[1][j];
temp[1][j] = temp[1][j + 1];
temp[1][j + 1] = buf;
}
}
}
if (temp[0][0] == temp[1][0] && temp[0][1] == temp[1][1] && temp[0][2] == temp[1][2])
{
return true;
}
return false;
}
private:
int m_length; //长
int m_width; //宽
int m_height; //高
};
bool judgeCube(Cube& c1, Cube& c2);
int main()
{
/*创建一个立方体对象并赋值*/
Cube cube1;
cube1.setSize(10, 20, 30);
/*打印这个立方体对象的面积和体积*/
cout << "面积为:" << cube1.calculateS() << endl;
cout << "体积为:" << cube1.calculateV() << endl;
/*创建另一个立方体对象并赋值*/
Cube cube2;
cube2.setSize(20, 10, 30);
/*调用成员函数判断两个立方体是否相等*/
if (cube2.judgeCube(cube1) == true)
cout << "一样" << endl;
else
cout << "不一样" << endl;
/*创建第三个立方体并赋值*/
Cube cube3;
cube3.setSize(30, 20, 10);
/*调用全局函数判断两个立方体是否相等*/
if (judgeCube(cube3, cube1) == true)
cout << "一样" << endl;
else
cout << "不一样" << endl;
system("pause");
return 0;
}
bool judgeCube(Cube& c1, Cube& c2)
{
int temp[2][3] =
{
{c1.getLen(), c1.getWid(), c1.getHei()},
{c2.getLen(), c2.getWid(), c2.getHei()}
};
for (int i = 0; i < 3 - 1; i++) //先将两组数据从小到大排序
{
for (int j = 0; j < 3 - 1 - i; j++)
{
if (temp[0][j] > temp[0][j + 1])
{
int buf = temp[0][j];
temp[0][j] = temp[0][j + 1];
temp[0][j + 1] = buf;
}
if (temp[1][j] > temp[1][j + 1])
{
int buf = temp[1][j];
temp[1][j] = temp[1][j + 1];
temp[1][j + 1] = buf;
}
}
}
if (temp[0][0] == temp[1][0] && temp[0][1] == temp[1][1] && temp[0][2] == temp[1][2])
{
return true; //小对小、中对中、大对大,三者全部相等则两个立方体全等
}
return false;
}
这个算法可能不是很好,但却是本人自己想出来的(如有雷同纯属巧合),就像是小时候做对了一道数学题,挺开心的。(当然各位如果有更好的算法也不妨让鄙人开开眼)
练习案例2:点和圆的关系
设计一个圆类(Circle),和一个点类(Point),计算点和圆的关系
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Point
{
public:
/*设置xy*/
void setPoint(int x, int y)
{
m_x = x;
m_y = y;
}
/*获取xy*/
int getX()
{
return m_x;
}
int getY()
{
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(int x, int y)
{
m_center.setPoint(x, y);
}
/*获取圆心*/
int getX()
{
return m_center.getX();
}
int getY()
{
return m_center.getY();
}
private:
int m_r;
Point m_center;
};
void isInCir(Circle& c, Point& p);
int main()
{
Circle c1;
c1.setCenter(0, 0);
c1.setR(10);
Point p1;
p1.setPoint(0, 11);
isInCir(c1, p1);
system("pause");
return 0;
}
void isInCir(Circle& c, Point& p)
{
/*计算两点间距离的平方*/
int distance =
(c.getX() - p.getX()) * (c.getX() - p.getX()) +
(c.getY() - p.getY()) * (c.getY() - p.getY());
/*计算半径的平方*/
int rDistance = c.getR() * c.getR();
/*判断相对关系*/
if (distance == rDistance) //两者都先平方再判断大小,免去了开根号的运算。同时解决了两个浮点数不好判断相等的问题
{
cout << "点在圆上" << endl;
}
else if (distance > rDistance)
{
cout << "点在圆外" << endl;
}
else
{
cout << "点在圆内" << endl;
}
}
对象的初始化和清理
C++中的对象在创建过程中其实还有初始化的过程,包括销毁时也有对应的处理。说得简单一些,就是对象在创建的过程中,会调用初始化的回调函数。之前我们并没有编写这类回调函数,是因为编译器会为我们提供默认的回调函数,默认的回调函数什么都不会做。
构造函数和析构函数
构造函数和析构函数就是上面说的回调函数,在对象创建、销毁过程中会被自动调用。
对象的初始化和清理是两个非常重要的工作:
- 如果对象在初始化之前就被使用,很可能会发生错误
- 如果对象申请了动态内存,在销毁对象前忘了释放内存就会发生内存泄漏
C++提供了构造函数和析构函数来解决上述问题,这两个函数会被编译器自动调用,完成对象的初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构函数,编译器会提供默认的构造和析构函数。
编译器提供的构造和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用
- 析构函数:主要作用于对象销毁前系统自动调用,执行一些清理工作
构造函数语法:类名(){}
- 构造函数没有返回值,也不需要写返回值类型(void)
- 函数名称和类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在对象创建时会自动调用构造函数,且只会调用一次,无需手动调用。
析构函数语法:~类名(){}
- 析构函数没有返回值,也不需要写返回值类型
- 函数名为类名前面加上~符号
- 析构函数不能有参数,因此也不可以重载
- 程序在对象销毁时会自动调用析构函数,且只会调用一次,无需手动调用。
注:这两个函数同样有权限,最好都设为公共权限,以免带来不必要的麻烦
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person
{
public:
/*构造函数*/
Person()
{
cout << "Person构造函数调用" << endl;
}
/*析构函数*/
~Person()
{
cout << "Person析构函数调用" << endl;
}
};
void test()
{
Person p1;
}
int main()
{
test();
system("pause");
return 0;
}
解释:
-
对象p1会在test函数中被创建,因此在创建的时候会自动调用一次构造函数,打印“Person构造函数调用”。
-
由于p1对象是局部变量存放在栈中,因此test函数调用结束后会被系统自动释放,释放时会自动调用一次析构函数,打印“Person析构函数调用”。
构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
三种调用方式:
- 括号法
- 显示法
- 隐式转换法
示例:
1、括号法
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person
{
public:
/*构造函数*/
Person() //无参构造函数(默认构造函数)
{
cout << "Person无参构造函数调用" << endl;
}
Person(int a) //有参构造函数
{
m_age = a;
cout << "Person有参构造函数调用" << endl;
}
Person(const Person& p) //拷贝构造函数
{
m_age = p.m_age;
cout << "Person拷贝构造函数调用" << endl;
}
/*析构函数*/
~Person()
{
cout << "Person析构函数调用" << endl;
}
/*属性*/
int m_age;
};
void test()
{
/*1、括号法*/
Person p1; //默认构造函数调用
Person p2(10); //有参构造函数调用
Person p3(p2); //拷贝构造函数调用
cout << "p2年龄为:" << p2.m_age << endl;
cout << "p3年龄为:" << p3.m_age << endl;
/*2、显示法*/
/*3、隐式转换法*/
}
int main()
{
test();
system("pause");
return 0;
}
输出:
Person无参构造函数调用
Person有参构造函数调用
Person拷贝构造函数调用
p2年龄为:10
p3年龄为:10
Person析构函数调用
Person析构函数调用
Person析构函数调用
注意:
- 在调用无参构造函数的时候,不要加括号,如:
Person p1();
。因为编译器会认为这句语句是一个函数的声明。 - 注意拷贝构造函数的参数,一定是const修饰的,且是引用传递的
2、显示法
void test()
{
/*1、括号法*/
/*2、显示法*/
Person p1; //无参构造
Person p2 = Person(10); //有参构造
Person p3 = Person(p2); //拷贝构造
/*3、隐式转换法*/
}
注意:
- 本例中
Person(10)
为匿名对象,特点:在当前行执行完毕后,系统会立即回收匿名对象 - 不要利用拷贝构造函数来初始化匿名对象,如:
Person(p3);
,编译器会将这句语句等价成Person p3;
3、隐式转化法
void test()
{
/*1、括号法*/
/*2、显示法*/
/*3、隐式转换法*/
Person p2 = 10; //有参构造
Person p3 = p2; //无参构造
}
注意:
- 在C++中有一个关键字
explicit
,可以用来修饰某个类的有参(拷贝)构造函数,被修饰的类将无法被隐式转换法创建、
例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person
{
public:
explicit Person(int a) //加explicit修饰,防止隐式法创建对象
{
}
Person(const Person& p)
{
}
int m_age;
};
int main()
{
Person p1(10);
Person p2 = Person(10);
//Person p3 = 10; 语法错误
system("pause");
return 0;
}
注:
- 如果拷贝构造函数也用
explicit
关键字修饰,那么显示法也无法创建对象
拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常由三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传递
- 以值方式返回局部对象
示例:
/*1、使用一个已经创建完毕的对象来初始化一个新对象*/
void test1()
{
Person p1;
Person p2(p1);
}
/*2、值传递的方式给函数参数传递*/
void func2(Person p)
{
}
void test2()
{
Person p1;
func2(p1);
}
/*3、以值方式返回局部对象*/
Person func3()
{
Person p;
return p;
}
void test3()
{
Person p1 = func3();
}
总结:
- 第一种情况是主动创建新对象(拷贝构造),这也是我们最容易理解的情况
- 第二种情况是值传递的时候,被调用函数要在栈中新建一个对象副本(形参)
- 第三种情况是函数返回对象,return的动作同样会在栈中创建临时变量
构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
- 如果用户定义有参构造函数,C++不再提供默认无参构造函数,但会提供默认拷贝构造函数
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
总结:
- 默认提供的构造函数和析构函数都是空实现,什么事情都不会做。当用户编写了相应函数,则调用用户编写的函数。
- 默认提供的拷贝构造函数会把类中的全部属性复制过来,如果有特殊需求则需要自己编写拷贝构造函数。
- 本小节就是一些死规矩,记住即可。
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person
{
public:
/*构造函数*/
Person()
{
cout << "Person无参构造函数调用" << endl;
}
Person(string name, int age)
{
m_name = new string(name);
m_age = age;
cout << "Person有参构造函数调用" << endl;
}
//Person(const Person& p) //这里我们使用默认的拷贝构造函数
//{
//}
/*析构函数*/
~Person()
{
delete m_name; //释放动态内存
m_name = NULL;
cout << "Person析构函数调用" << endl;
}
/*属性*/
int m_age;
string* m_name;
};
int main()
{
Person p1("张三", 18);
Person p2(p1); //下面两句正常输出
cout << "p1姓名:" << *p1.m_name << ",p1年龄:" << p1.m_age << endl;
cout << "p2姓名:" << *p2.m_name << ",p2年龄:" << p2.m_age << endl;
*p2.m_name = "李四"; //修改一下p2的姓名
/*结果发现p1的姓名也跟着被修改了*/
cout << "p1姓名:" << *p1.m_name << ",p1年龄:" << p1.m_age << endl;
cout << "p2姓名:" << *p2.m_name << ",p2年龄:" << p2.m_age << endl;
system("pause");
return 0;
}
在关闭程序窗口的时候还会报错:引发了异常: 读取访问权限冲突
原因分析:
-
默认的拷贝构造函数会做浅拷贝操作,把对象属性的值一一复制过去。在本例中,Person类含有一个指针成员,用来指向一块从堆中申请的内存。在创建对象p1的时候,调用了有参构造函数,从堆中申请了一块内存,并让m_name指向这块内存。而在创建对象p2的时候,使用了默认的拷贝构造函数,p2中的m_name指针变量复制了p1中m_name指针变量的值,导致这两个指针都指向了同一块内存。所以其中一个对象修改名字后,另一个对象的名字也会跟着改变。
-
同时,在退出main函数时,系统会释放栈中内存。因为p2后被创建,所以它先被销毁(栈先进后出),销毁时调用了析构函数,释放了堆中内存。然后当p1也被销毁时,同样要调用一次析构函数,此时堆中内存已经被p2释放掉了,p1又去释放,因而导致程序崩溃。
解决办法:深拷贝
重新编写一个拷贝构造函数,重新在堆区申请一块内存
Person(const Person& p)
{
m_name = new string(*p.m_name);
m_age = p.m_age;
}
初始化列表
作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2),属性3(值3)...{}
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person
{
public:
/*构造函数*/
Person():m_name("张三"), m_age(18)
{
}
/*属性*/
string m_name;
int m_age;
};
int main()
{
Person p1;
cout << "p1姓名:" << p1.m_name << ",p1年龄:" << p1.m_age << endl;
system("pause");
return 0;
}
初始化列表可以采用这样的形式:
Person(string name, int age):m_name(name), m_age(age)
{
}
调用时方法如下:Person p1("张三", 18);
但这样就和有参构造函数的功能重复了。
类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
例如:
class A
{};
class B
{
A a;
};
B类中有对象A作为成员,A为对象成员
那么创建B对象时,A与B的构造、析构顺序是谁先谁后?
示例:
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Phone
{
public:
Phone():m_name("苹果")
{
cout << "手机构造函数调用" << endl;
}
~Phone()
{
cout << "手机析构函数调用" << endl;
}
string m_name;
};
class Person
{
public:
Person() :m_name("张三")
{
cout << "人类构造函数调用" << endl;
}
~Person()
{
cout << "人类析构函数调用" << endl;
}
string m_name;
Phone m_phone;
};
void test()
{
Person p1;
cout << p1.m_name << "拿着" << p1.m_phone.m_name << "手机" << endl;
}
int main()
{
test();
system("pause");
return 0;
}
输出:
手机构造函数调用
人类构造函数调用
张三拿着苹果手机
人类析构函数调用
手机析构函数调用
总结:
- 当一个类包含其他类,创建此类对象的时候会先构造对象成员,再构造自身
- 当销毁此对象的时候,会先析构自身,再析构对象成员
静态成员
静态成员就是在成员变量和成员函数前加上static关键字,称为静态成员。
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
示例1:静态成员变量
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person
{
public:
static int m_a; //类内声明
private:
static int m_b;
};
int Person::m_a = 10; //类外初始化
int Person::m_b = 100;
int main()
{
/*案例1:p1、p2共享静态成员变量*/
Person p1;
cout << p1.m_a << endl;
Person p2;
p2.m_a = 20;
cout << p1.m_a << endl; //输出20
/*案例2:通过类名访问静态成员变量*/
Person::m_a = 30;
cout << Person::m_a << endl; //输出30
/*案例3:静态成员变量的访问权限*/
//Person::m_b = 200; 私有权限,类外无法访问
system("pause");
return 0;
}
总结:
- 静态成员变量被所有此类对象共享内存
- 静态成员变量可以使用类名来访问
- 静态成员变量可以设置不同的访问权限
示例2:静态成员函数
#include <iostream>
#include <string>
#include <ctime>
using namespace std;
class Person
{
public:
static void func() //静态成员函数
{
cout << "静态成员函数调用" << endl;
cout << "m_a = " << m_a << endl;
//cout << "m_b = " << m_b << endl; 静态成员函数无法访问非静态成员变量
}
static int m_a; //静态成员变量
int m_b; //非静态成员变量
};
int Person::m_a = 10;
int main()
{
/*案例1:通过对象访问静态成员函数*/
Person p1;
p1.func();
/*案例2:通过类名访问静态成员函数*/
Person::func();
system("pause");
return 0;
}
总结:
- 静态成员函数无法访问非静态成员变量
- 静态成员函数可以被类名访问
- 静态成员函数同样有访问权限
对象模型和this指针
成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
1、空对象占用的内存空间是多少?
#include <iostream>
#include <string>
using namespace std;
class Person
{
};
int main()
{
cout << sizeof(Person) << endl;
system("pause");
return 0;
}
答案是1个字节:
- C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
- 每个空对象也应该有一个独一无二的内存地址
2、含有非静态成员变量的对象占用多少内存?
class Person
{
int a;
};
答案是4字节。
3、含有静态成员变量的对象占用多少内存?
class Person
{
int a;
static int b;
};
答案还是4字节,因为静态成员变量不属于类对象上
4、含有非静态成员函数的对象占用多少内存?
class Person
{
int a;
static int b;
void func()
{}
};
答案依然是4字节,非静态成员函数不属于类对象上
5、含有静态成员函数的对象占用多少内存?
class Person
{
int a;
static int b;
void func()
{}
static func2()
{}
};
答案仍然是4字节
总结:
- 只有非静态成员变量,属于类的对象上
- 静态成员变量和成员函数都不属于类对象上,它们被所有的类对象共用一份
this指针概念
通过上一小节我们知道,在C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个类型的对象会共用一块代码
那么问题来了:这一块代码是如何区分是哪一个对象在调用自己?
C++通过提供特殊的对象指针:this指针,来解决这类问题。this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途
- 当形参和成员变量同名时,可以用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
示例1:解决成员函数的形参和成员变量重名的问题
class Person
{
public:
void func(int a)
{
this->a = a;
}
int a;
};
示例2:返回对象本身用*this
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
Person() :a(0), b(0)
{}
Person& addA(int a) //返回引用
{
this->a += a;
return *this;
}
Person addB(int b) //返回对象
{
this->b += b;
return *this;
}
int a;
int b;
};
int main()
{
Person p;
p.addA(10).addA(10).addA(10);
cout << "a = " << p.a << endl;
p.addB(10).addB(10).addB(10);
cout << "b = " << p.b << endl;
system("pause");
return 0;
}
输出:
a = 30
b = 10
总结:
- 这叫链式编程思想,一个对象可以无限追加的调用方法
- 想用这种方式,成员函数一定要引用返回。如果是值返回,每次返回都会创建一个新的对象,新的对象不是原来是对象p,因此累加的值不会加到p对象中的b上
结论:哪个对象调用的成员函数,此时this指针就指向该对象
空指针访问成员函数
C++中空指针也可以调用成员函数,但要注意成员函数中有没有用到this指针
如果用到了this指针,就需要加以判断保证代码的健壮性
示例:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
void show()
{
cout << "I am Person class" << endl;
}
void showAge()
{
/*访问m_age实际上等于访问this->m_age,如果是空指针调用,就会程序崩溃*/
cout << "the age is:" << m_age << endl;
}
int m_age;
};
int main()
{
Person* p = NULL; //创建Person类型的空指针
p->show(); //这句正常运行
p->showAge(); //这句使程序崩溃
system("pause");
return 0;
}
改进措施:
class Person
{
public:
void show()
{
cout << "I am Person class" << endl;
}
void showAge()
{
if (this == NULL) //如果是空指针在访问,直接返回
return;
cout << "the age is:" << m_age << endl;
}
int m_age;
};
const 修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修饰成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
示例:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
/* this 指针的本质是指针常量,像
this = NULL; 这样的语句是不允许的*/
void setB() const //加const修饰,变为常函数
{
//this->m_a = 10; 报错,常对象不可以修改普通成员变量
this->m_b = 10; //常函数可以修改加 mutable 修饰的成员变量
}
void setA()
{
this->m_a = 10;
}
int m_a;
mutable int m_b;
};
int main()
{
const Person p; //const修饰,变为常对象
//p.m_a = 10; 报错,常对象不可以修改普通成员变量
p.m_b = 10; //常对象可以修改加 mutable 修饰的成员变量
/*常对象只能访问常函数*/
p.setB();
//p.setA(); 报错
system("pause");
return 0;
}
总结:
- this指针的本质是指针常量:
Person const *this = 调用成员函数的那个对象;
。因此,修改this指针的指向是不可以的 - 加const修饰的成员函数,就会使this指针指向的值也不可修改:
const Person const *this = 调用成员函数的那个对象;
友元
通过之前的学习我们了解到,如果一个类中的某些属性设置了私有权限,那么这些属性只能在这个类内被访问,在类外是无法访问这些私有属性的,更别说其他的类。但在本节,我们要学习一种新的特性,它可以使得某个函数或者类访问到某个类的私有属性,这个特性就是友元。
友元特性使得不同的类不再是彼此孤立的状态,可以存在联系。
友元的关键词:friend
友元的三种实现形式:
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元
示例:
#include <iostream>
#include <string>
using namespace std;
class Person
{
/*这就是全局函数做友元的声明
被声明的函数可以访问本类的保护、私有属性*/
friend void goodFriend(Person& p);
friend int main();
public:
void init()
{
name = "张三";
car = "奔驰";
secret = "捡到一百块";
}
string name;
protected:
string car;
private:
string secret;
};
void goodFriend(Person &p)
{
cout << "好友的姓名为:" << p.name << endl;
cout << "好友的汽车为:" << p.car << endl;
cout << "好友的秘密为:" << p.secret << endl;
}
int main()
{
Person p1;
p1.init();
p1.car = "宝马";
goodFriend(p1);
system("pause");
return 0;
}
输出:
好友的姓名为:张三 好友的汽车为:宝马 好友的秘密为:捡到一百块
类做友元
示例:
#include <iostream>
#include <string>
using namespace std;
class Person
{
/*这就是类做友元的声明,被声明的类可以
通过它的所有成员函数访问本类的保护、私有属性*/
friend class Friend;
public:
/*添加构造函数,在创建后属性被赋值*/
Person()
{
name = "张三";
car = "奔驰";
secret = "捡到一百块";
}
string name;
protected:
string car;
private:
string secret;
};
class Friend
{
public:
/*添加构造函数,在堆区创建一个Person对象,
并让goodFriend指针指向它*/
Friend()
{
goodFriend = new Person;
}
/*Friend类通过成员函数访问Person类中的保护、私有属性*/
void visitFriend()
{
cout << "好友的姓名为:" << goodFriend->name << endl;
cout << "好友的汽车为:" << goodFriend->car << endl;
cout << "好友的秘密为:" << goodFriend->secret << endl;
}
Person* goodFriend;
};
int main()
{
Friend f1;
f1.visitFriend();
system("pause");
return 0;
}
输出:
好友的姓名为:张三 好友的汽车为:奔驰 好友的秘密为:捡到一百块
成员函数做友元
成员函数做友元就更简单了。一个类做友元,那么这个类中所有的成员函数都可以访问另一个类中的保护、私有属性(为什么只说成员函数能访问?不然呢,难道类中的变量也能访问吗)。而成员函数做友元,就只有被声明的成员函数能访问。
示例:
#include <iostream>
#include <string>
using namespace std;
class Person;
class Friend;
class Person
{
/*这就是成员函数做友元的声明,被声明的
成员函数可以访问本类的保护、私有属性*/
friend void Friend::visitFriend();
public:
/*添加构造函数,在创建后属性被赋值*/
Person()
{
name = "张三";
car = "奔驰";
secret = "捡到一百块";
}
string name;
protected:
string car;
private:
string secret;
};
class Friend
{
public:
/*添加构造函数,在堆区创建一个Person对象,
并让goodFriend指针指向它*/
Friend()
{
goodFriend = new Person;
}
/*Friend类通过成员函数访问Person类中的保护、私有属性*/
void visitFriend()
{
cout << "好友的姓名为:" << goodFriend->name << endl;
cout << "好友的汽车为:" << goodFriend->car << endl;
cout << "好友的秘密为:" << goodFriend->secret << endl;
}
void visitFriend2()
{
cout << "好友的姓名为:" << goodFriend->name << endl;
// cout << "好友的汽车为:" << goodFriend->car << endl;
// cout << "好友的秘密为:" << goodFriend->secret << endl;
}
Person* goodFriend;
};
int main()
{
Friend f1;
f1.visitFriend();
system("pause");
return 0;
}
结果发现添加了成员函数做友元的声明还是报错无法访问,编译不通过。
解决方法:
后来发现,如果要让成员函数做友元,成员函数必须类内定义,类外实现。且做友元的那个类必须定义在前头,特别难搞。
#include <iostream>
#include <string>
using namespace std;
class Person;
class Friend;
/*Friend类必须定义在Person类的前面,不然会报错*/
class Friend
{
public:
/*添加构造函数,在堆区创建一个Person对象,
并让goodFriend指针指向它*/
Friend();
/*Friend类通过成员函数访问Person类中的保护、私有属性*/
void visitFriend();
void visitFriend2();
Person* goodFriend;
};
class Person
{
/*这就是成员函数做友元的声明,被声明的
成员函数可以访问本类的保护、私有属性*/
friend void Friend::visitFriend();
public:
/*添加构造函数,在创建后属性被赋值*/
Person()
{
name = "张三";
car = "奔驰";
secret = "捡到一百块";
}
string name;
protected:
string car;
private:
string secret;
};
/*这个构造函数同样要写在外头,不然报错*/
Friend::Friend()
{
goodFriend = new Person;
}
void Friend::visitFriend()
{
cout << "好友的姓名为:" << goodFriend->name << endl;
cout << "好友的汽车为:" << goodFriend->car << endl;
cout << "好友的秘密为:" << goodFriend->secret << endl;
}
void Friend::visitFriend2()
{
cout << "好友的姓名为:" << goodFriend->name << endl;
/*下两句会报错*/
//cout << "好友的汽车为:" << goodFriend->car << endl;
//cout << "好友的秘密为:" << goodFriend->secret << endl;
}
int main()
{
Friend f1;
f1.visitFriend();
f1.visitFriend2();
system("pause");
return 0;
}
输出:
好友的姓名为:张三
好友的汽车为:奔驰
好友的秘密为:捡到一百块
好友的姓名为:张三
知识点补充
- 友元的关系的单向的
- 友元不能被继承
- 友元不具有传递性