本文是学习“黑马程序员c++”时做的笔记,如有问题请指出。
类与对象
c++面向对象的三大特性为:封装、继承、多态
c++认为万事万物皆为对象,对象有其属性和行为。
具有相同性质的对象,我们可以抽象成类。
- 例如,人属于人类,具有姓名、年龄等属性,有走、跑等行为。
1. 封装
1.1 封装的意义
- 将属性和行为作为一个整体,表现生活中的事物;
- 将属性和行为加以权限控制。
意义一:基本语法:
class className
{
accessSpecifier://访问权限-private, public or protected
dataMembers; //属性-variables to be used
memberFunctions(){} //行为-methods to access data members
};
例:创建一个圆类,求具体圆对象的周长。
#include <iostream>
using namespace std;
const double PI = 3.14;//定义圆周率
class Circle//类名一般首字母大写,增加可读性
{
public:
int m_r;//属性:半径
double Circumference()//行为(或成员函数):获取圆的周长
{
return 2 * PI * m_r;
}
};
int main()
{
Circle c1;//通过Circle类创建具体的圆
c1.m_r = 10;//给c1的m_r属性赋值
cout << "The circumference of this circle is: " << c1.Circumference() << endl;
return 0;
}
意义二:三种访问权限:
-
public 公共权限————成员 类内可以访问,类外也可以访问。
-
protected 保护权限————成员 类内可以访问,类外不可访问;子类可访问父类的私有内容。
-
private 私有权限————成员 类内可以访问,类外不可访问;子类不能访问父类的私有内容。
例:封装的权限控制:
/*Wanging: this code is buggy.*/
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
string m_Name;//姓名
protected:
string m_Car;//汽车
private:
int m_Pswd;//银行卡密码
public:
void func()
{
m_Name = "张三";
m_Car = "劳斯莱斯";
m_Pswd = 114514;
}
};
int main()
{
Person p1;
p1.m_Name = "李四";
p1.m_Car = "拖拉机";//无法访问
p1.m_Pswd = 1919810;//无法访问
p1.func();//无法调用
}
1.2 struct和class的区别
唯一区别就在于默认的访问权限不同。
- struct 默认权限为公共
- class 默认权限为私有
(注:c++11开始,struct可以修改权限,也有封装、继承和多态的特性)
1.3 成员属性设置为私有的好处
优点1:将所有成员属性设为私有,可以自己控制读写权限。
优点2:对于写权限,我们可以检测数据的有效性。
例:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public://创建一个对外的“接口”
//设置姓名的函数
void setName(string name)
{
m_Name = name;
}
//读取姓名的函数
string getName()
{
return m_Name;
}
//获取年龄
int getAge()
{
m_Age = 0;//初始化为0
return m_Age;
}
//设置情人
void setLover(string lover)
{
m_Lover = lover;
}
}
private:
{
string m_Name;//姓名 可读可写
int m_age;//年龄 只读
string m_Lover;//对象 只写
};
int main()
{
Person p1;
p1.setName("张三");
cout << "姓名为:" << p1.getName() << endl;
cout << "年龄为:" << p1.getAge() << endl;
//p.setAge();
//没有给出setAge函数,故无法修改
p.setLover("大力王byd");
//cout << "情人为:" << p1.getLover() << endl;
//没有给出getLover函数,故无法访问
return 0;
}
/*如果要检测数据的有效性,可以在public创建函数,然后在函数内用if判断*/
练习:判断点和圆的关系
#include <iostream>
#include <string>
#include <cmath>
using namespace std;
class Point//定义一个点类
{
public:
//设置x的值
void setX(int x)
{
m_X = x;
}
//获取x
int getX()
{
return m_X;
}
//设置y的值
void setY(int y)
{
m_Y = y;
}
//获取y
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(Point center)
{
m_Center = center;
}
//获取圆心
Point getCenter()
{
return m_Center;
}
private:
int m_R;//半径
Point m_Center;//圆心
//类的嵌套
};
//判断点和圆的关系
void isInCircle(Circle &c, Point &p)
{
//计算两点之间距离的平方
int dSq =
pow((c.getCenter().getX() - p.getX()), 2) +
pow((c.getCenter().getY() - p.getY()), 2);
//计算半径的平方
int rSq = pow(c.getR(), 2);
if(dSq == rSq)
{
cout << "点在圆上" << endl;
}
else if(dSq > rSq)
{
cout << "点在圆外" << endl;
}
else
cout << "点在圆内" << endl;
}
int main()
{
Circle c;
c.setR(10);
//创建(10,10)的圆心
Point center;
center.setX(10);
center.setY(10);
c.setCenter(center);
//创建点
Point p;
p.setX(10);
p.setY(11);
isInCircle(c,p);
return 0;
}
注:可以把class扔进一个头文件xxx.h中,在cpp文件中用#include "xxx.h"来使用。
- 头文件中的class中的成员函数仅需声明,在cpp文件中再定义。
/*xxx.h*/
#pragma once//防止重复引用
#include <iostream>
using namespace std;
class Circle
{
public:
void setX(int x);
private:
int m_X;
int m_Y;
};
/*xxxx.cpp*/
#include "xxx.h"
void Circle::setX(int x)//代表setX是Circle作用域的函数
{
m_X = x;
}
2. 对象的初始化和清理
生活中我们买的电子产品基本会有出厂设置,在某一天我们不用的时候也会删除一些数据来保证安全。
c++中的每个对象也会有初始设置,以及对象销毁前的清理数据的设置。
2.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题。
- 一个对象或者变量如果没有初始状态,对其使用的后果未知;
- 使用完一个对象或变量,没有及时清理,也会造成一定的安全问题。
c++利用了构造函数和解析函数解决上述问题,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作。该工作是编译器强制要我们做的事情,如果我们不提供构造和析构,编译器会提供空实现。
- 构造函数:主要用于在创建对象时为对象的成员属性赋值。构造函数由编译器自动调用,无需手动调用。
- 析构函数:在对象销毁前系统自动调用,执行清理工作。
构造函数语法:ClassName(){}
- 没有返回值,也不写void;
- 函数名称与类名相同;
- 构造函数可以有参数,因此可以重载;
- 程序在调用对象的时候会自动调用构造,无需手动调用,而且只会调用一次。
析构函数语法:~ClassName(){}
- 没有返回值,也不写void;
- 函数名称与类名相同且在前面加上~;
- 析构函数不可以有参数,因此不能重载;
例1:构造函数
#include <iostream>
using namespace std;
class Person
{
public:
//构造函数
Person()
{
cout << "Person构造函数的调用" << endl;
}
//析构函数
~Person()
{
cout << "Person的析构函数调用" << endl;
}
};
//创建对象
void test01()
{
Person p;//在栈上的数据,test01执行完毕后,释放这个对象
}
int main()
{
test01();
Person pp;
system("Pause");
return 0;
}
/*输出结果:
Person构造函数的调用
Person析构函数的调用
Person构造函数的调用
Press any key to continue . . .
Person析构函数的调用
说明在创建对象时构造函数自动调用了,
并且p在test01()执行完之后被销毁,
而pp在main函数执行完之后才被销毁*/
2.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)//必须加const,而且后面是地址传递
{
//将传入的人身上的所有属性拷贝到我身上
age = p.age;
cout << "拷贝函数调用" << endl;
}
int age;
};
//调用
void test01()
{
Person p1;//默认构造函数调用(不要有括号,否则会被编译器认为是函数声明)
//1.括号法
Person p2(10);//调用有参构造函数
Person p3(p2);//调用拷贝构造函数
cout << "p2的年龄为:" << p2.age << endl;
cout << "p3的年龄为:" << p3.age << endl;//p2的age被拷贝到了p3
//2.显示法
Person p11;
Person p22 = Person(10);//有参构造
Person p33 = Person(p22);//拷贝构造
//注:Person(10)会创建一个匿名对象。特点:当前行执行结束后,系统会立即回收该对象
//并且不要用拷贝构造函数来初始化匿名对象,否则系统会报错:重定义
//3.隐式转换法
Person p4 = 10;//会转换成Person p4 = Person(10)//有参构造
Person p5 = p4;//拷贝构造
/*以上三种方法可任选一种使用*/
}
int main()
{
test01();
return 0;
}
2.3 拷贝构造函数的调用时机
当以拷贝的方式初始化对象时会调用拷贝构造函数,这里需要注意两个关键点,分别是以拷贝的方式和初始化对象。初始化对象是指,为对象分配内存后第一次向内存中填充数据,这个过程会调用构造函数,对象被创建后必须立即初始化。也就是说只要创建对象就会调用构造函数。
三种情况:
- 使用一个已创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
例:
#include <iostream>
using namespace std;
class Person
{
public:
//构造函数
Person()
{
cout << "Person默认构造函数调用" << endl;
}
Person(int age)
{
cout << "Person有参构造函数调用" << endl;
m_Age = age;
}
Person(const Person &p)
{
cout << "Person拷贝构造函数调用" << endl;
m_Age = p.m_Age;
}
~Person()
{
cout << "Person析构函数调用" << endl;
}
int m_Age;
};
//1、使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
Person p1(20);
Person p2(p1);
cout << "P2的年龄为:" << p2.m_Age << endl;
}
//2、值传递的方式给函数参数传值(相当于Person p = p拷贝构造函数的隐式写法)
void doWork(Person p)//doWork的p是doWork的局部变量,局部变量接受外部的对象时,
{ //使用拷贝构造来把数据复制一份给自己
}
void test02()
{
Person p;
doWork(p);
}
//3、值方式返回局部对象
Person doWork2()
{
Person p3;
cout << (int*)&p3 << endl;
return p3;
}
void test03()
{
Person p4 = doWork2();
cout << (int*)&p4 << endl;
}
int main()
{
test01();
test02();
test03();
return 0;
}
2.4 构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
调用规则如下:
- 如果用户有定义有参构造函数,c++不会再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户自定义拷贝构造函数,c++不会再提供其他构造函数
不放例子了,之前有提过。
2.5 深拷贝和潜拷贝
面试经典问题之一
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区中重新申请空间,进行拷贝操作
例:
//沿用上一节的类
public:
Person(int age, int height)
{
m_Age = age;
new int(height);//堆区数据由程序员手动创建,在析构函数被调用前要手动释放
cout << "Person的有参构造函数调用" << endl;
}
~Person()//析构函数
{
if (m_Height != NULL)
{
delete m_Height;
m_Height = NULL;//防止野指针出现
}
}
- 如果利用编译器提供的拷贝构造函数,将会执行浅拷贝操作。
2.6 初始化列表
语法:构造函数(): 属性1(值1), 属性2(值2)...{}
例:
#include <iostream>
using namespace std;
class Person
{
public:
/*传统初始化操作
Person(int a, int b, int c)
{
m_A = a;
m_B = b;
m_C = c;
}*/
//初始化列表
Person(int a, int b, int c):m_A(a), m_B(b), m_C(c)
{}
int m_A;
int m_B;
int m_C;
};
void test01()
{
Person p(10, 20, 30);
cout << p.m_A << endl;
cout << p.m_B << endl;
cout << p.m_C << endl;
}
int main()
{
test01();
return 0;
}
使用初始化列表的好处:
-
- 类成员中存在常量时,只能初始化而不能赋值。
-
- 类成员存在引用时,也只能初始化。
-
- 在大型项目中,类中成员变量极多的情况下,初始化列表的效率更高。
2.7 类对象作为成员
成员可以使另一个类的对象,我们称该成员为对象成员。如:
class A{};
class B
{
A a;
}
例:
#include <iostream>
#include <string>
using namespace std;
//手机类
class Phone
{
public:
Phone(string pBrand)//构造函数
{
m_Brand = pBrand;
}
string m_Brand;
};
//人 类
class Person
{
public:
//相当于Phone m_Phone = pName;以下是一种隐式转换
Person(string name, string pName):m_Name(name),m_Phone(pName)
{
}
string m_Name;
Phone m_Phone;
};
void test01()
{
Person p("张三", "iPhone114514");
cout << p.m_Name << "拿着" << p.m_Phone.m_Brand;
}
int main()
{
test01();
return 0;
}
当其他类对象作为本类的成员时,其他类对象的构造函数先于本类完成执行
2.8 静态成员
静态成员就是在成员变量、函数前加上static。
-
静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
-
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
例1:静态成员变量
#include <iostream>
using namespace std;
class Person
{
public:
//类内声明
static int m_A;
};
//类外初始化。要写Person作用域。
int Person::m_A = 114514;
void test01()
{
Person p;
cout << p.m_A << endl;
//通过p2来修改m_A数据,再通过p访问m_A
Person p2;
p2.m_A = 200;
//会发现p的m_A也被修改了,从而说明所有对象共享同一份数据
cout << p.m_A << endl;
}
void test02()
{
//静态成员变量有两种访问方式
//1、通过对象进行访问
//Person p;
//cout << p.m_A << endl;
//2、通过类名进行访问
cout << Person::m_A << endl;
}
int main()
{
test01();
return 0;
}
例2:静态成员函数
#include <iostream>
using namespace std;
class Person
{
public:
//静态成员函数
static void func()
{
m_A = 100;//静态成员函数可以访问 静态成员变量
//m_B = 200; 会报错。不可访问 非静态成员变量
cout << "静态成员函数调用" << endl;
}
static int m_A;//静态成员变量
int m_B;//非静态!!此变量属于特定的对象,func()不知道为哪一个对象的m_B赋值,于是会报错
}
void test01()
{
//1、通过对象访问(与例1类似)
//2、通过类名访问
Person::func();
}
int main()
{
test01();
return 0;
}
3. c++对象模型和this指针
3.1 成员变量和成员函数分开存储
类内的的成员变量和成员函数分开存储。只有非静态成员才属于类的对象上。
例:
#include <iostream>
using namespace std;
class Person1{};//空类
void test01()
{
Person1 p1;
//空对象占用内存空间是 1
//c++编译器会给每个空对象也分配一个字节空间,为了区分空对象站内存的位置。
//每个空对象也有一个独一无二的内存地址
cout << "Size of p is "<< sizeof(p1) << endl;
}
class Person2
{
int m_A;//非静态成员变量 属于类对象上
static int m_B;//静态成员变量 不属于类对象上
void func(){}//非静态成员函数 不属于类对象上
};
int Person2::m_B = 114514;
void test02()
{
Person2 p2;
//此处输出4,包括m_A的长度,不包括m_B和func()的长度。
cout << "Size of p2 is " << sizeof(p2) << endl;
}
int main()
{
test01();
test02();
return 0;
}
3.2 this指针概念
this指针指向被调用的成员函数所属的对象。它不需要定义,可直接使用。它被隐含在每一个非静态成员函数内。
用途:
- 当形参和成员变量同名时,可用this指针区分。
- 在类的非静态成员函数中返回对象本身,可用return *this。\
例:
#include <iostream>
using namespace std;
class Person
{
public:
Person(int age)
{
this->age = age;//this指向的age是成员变量age,与形参age区分开来
}
int age;
Person& PersonAddAge(Person &p)
{
//将另一个“人”的年龄加到自己年龄上(好怪哦)
this->age += p.age;
//this是指向p2的指针,*this是p2本体
return *this;
}
};
//1、解决名称冲突
void test01()
{
Person p1(10);
cout << "p1的年龄为" << p1.age << endl;
}
//返回对象本身用*this
void test02()
{
Person p1(10);
Person p2(10);
//链式编程思想
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);//加三次
cout << "p2的年龄为" << p2.age << endl;
}
int main()
{
test01();
test02();
return 0;
}
注意:对于PersonAddAge函数,如果前面是值传递而非引用传递,则后面输出p2的结果是20。这是因为值传递会创建新的对象,而引用传递则直接操作原始对象,第一次调用p2.PersonAddAge(p1)时,实际上是将p2和p1的age相加得到20,然后返回一个新的Person对象p(其age为20)(实际上这个对象的名称是未知的,也不重要),但这个新的对象并没有赋值给p2,所以p2的age仍然是初始值10。接着连续调用PersonAddAge函数两次,每次都是将新的Person对象p(age为20)与p1的age相加,结果都是20,然后将结果返回给一个新的Person对象p,但这些新的对象并没有赋值给p2,所以p2的age仍然是20。
3.3 空指针访问成员函数
c++空指针可以调用成员函数,但是要注意有没有用到this指针。如果有,需要加以判断,保证代码的健壮性。
例:
#include <iostream>
using namespace std;
class Person
{
public:
void showClassName()
{
cout << "This is Person class." << endl;
}
void showPersonAge()
{
cout << "Age = " << m_Age << endl;
}
int m_Age;
};
void test01()
{
Person * p = NULL;
p->showClassName();//不会报错
//p->showPersonAge();//会报错
}
int main()
{
test01();
return 0;
}
p->showPersonAge()报错的原因是:该函数内的m_Age是默认为this->m_Age的,而this指针(也即p)是空的,因此报错
- 为了防范此bug,我们可以用if判断是否为空指针,若为指针直接return。
3.4 const修饰成员函数
1.常函数
- 被const修饰的成员函数就叫常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
2.常对象
- 被const修饰的对象称为常对象
- 常对象只能调用常函数
例:
#include <iostream>
using namespace std;
class Person
{
public:
//常函数
//在成员函数后加const,实际上修饰的是this指针的指向。
void showPerson() const
{
//this->m_A = 100;//会报错
this->m_B = 100;//不会报错
}
//普通成员函数
void func()
{}
int m_A;
mutable int m_B;//const不允许我改?诶,给你加个mutable!
};
void test01()
{
Person p;
p.showPerson();
}
//常对象
void test02()
{
const Person p;
//p.m_A = 100;//会报错
p.m_B = 100;//不会报错
//p.func();//会报错
p.showPerson();//不会报错
}
int main()
{
test01();
test02();
return 0;
}
- this指针的本质是常量指针,它指向的内容不可修改。