C++面向对象
二、C++面向对象
c++内存的四个区域
1.代码区
存放函数体的二进制的代码,由操作系统管理,两个特点 共享 可读。
2.全局区
存放全局变量、静态变量以及常量。
常量包括const修饰的全局常量和字符串常量
(全局变量和局部变量不会放在同一个内存区域中)
(全局变量和静态变量放在同一个内存区域中)
3.栈区
由编译器自动分配,存放函数参数d,局部变量,
注意:不要返回局部变量的地址。函数返回局部变量地址后第一次使用可以正常使用,这是因为编译器做了保留,第二次就不能调用了。(不要返回局部变量地址)
4.堆区
由程序员分配,也由程序员释放,如果程序员不释放,会在程序结束后由操作系统自动回收。
4.1 在堆中开辟空间new,如:
int * p = new int(10);
4.2 释放内存delete
delete p;
--p是指向堆中内存的指针。
如果p是一个数组指针 ,使用delete[] p;
引用(相当于起 别名 )
1.引用格式&
int b = 10;
int& a = b;
等同于a和b指向同一块空间。
2.注意事项
- 引用必须初始化
- 引用定义后,不可修改引用
3.用处
- 引用传递
&
,相当于指针传递;代码更加简洁
#include <iostream>
using namespace std;
//交换两个数字
void function(int& a,int& b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a = 1;
int b = 2;
function(a,b);
cout << a << b << endl;
}
- 返回引用
#include <iostream>
using namespace std;
//把b的值加到a上并返回a
int& add(int& a, int& b)
{
a = a + b;
return a;
}
int main()
{
int a = 1;
int b = 2;
int& temp = add(a, b);
cout << temp << endl;
}
注意:不要返回局部变量的引用(局部变量出了作用域会自动释放)
下面的代码虽然可以的得到正确的结果,但是他是不安全的,因为它访问了理论上已经被释放的变量temp。在部分编译器上可能无法得到正确运行。。
#include <iostream> using namespace std; //返回局部变量 int& function() { int temp = 1; return temp;//不安全的,不要这样写 } int main() { int& temp = function(); cout << temp << endl; cout << temp << endl; }
函数返回引用后,函数可以作为左值,即就当成是个变量。可进行如下的赋值操作,如:
#include <iostream>
using namespace std;
int& function(int& a)
{
a = 1;
return a;
}
int main()
{
int a = 0;
function(a) = 2; //相当于有 a = 2;
cout << a << endl;
}
程序执行结果为2。
4.引用的底层原理
引用传递的底层实际上是一个指针,其取地址和解引用操作是自动的。方便写程序。
int&
引用的实际类型为int* const ref
指针常量,这也是为什么引用不能修改的原因,因为指针的指向不可改变。
特殊写法(引用常量):
const int& a = 123;
解释:实际上就相当于 是
int temp = 123;
const int& ref = temp
这两行代码
函数
1.函数默认参数
参数有默认值时允许不传值,如果传了值,就不使用默认使用传递的及具体值。
基础代码(不用默认值)
//返回三个数的和
int add(int a, int b,int c)
{
return a + b + c;
}
int main()
{
int re = add(1, 2, 3);
cout << re << endl;
system("pause");
return 0;
}
使用默认值的代码
int add(int a, int b = 2,int c = 3) //使用了默认值b默认为2,c默认为3.
{
return a + b + c;
}
int main()
{
int re = add(1); //此处调用,实际相当于add(1,2,3);
cout << re << endl;
system("pause");
return 0;
}
书写规则,有默认值得参数一定要写在未提供默认值的参数后。
也可以在声明里面写默认,但是和函数定义时的默认值不能重复(声明时写了,定义时就不能写)。
int add(int, int = 1, int = 2);//后面的占位参数写法
int main()
{
int re = add(1);
cout << re << endl;
system("pause");
return 0;
}
int add(int a, int b, int c)
{
return a + b + c;
}
2.函数的占位参数
占位参数可以只写类型。目前了解,暂不涉及。
如上个代码add函数的参数就是占位参数,而且占位参数也可以有默认参数。
3.函数重载
条件:
- 在一个作用域下
- 函数名一样
- 形参不一样(要么个数不一样,要么类型不一样,要么类型的顺序不一样)
注意:对返回值没有要求。
易错点:
函数重载时尽量不要加默认参数,否则调用时可能出现二义性。尽量避免。
类class
1.使用
const double PI = 3.1415926;
//定义圆Circle类
class Circle
{
public: //声明下列成员的权限为public
double m_r;
double getZC() {
return 2 * m_r * PI;
}
};
int main()
{
Circle cir; //创建圆类的对象
cir.m_r = 2; //给内部成员变量r半径赋值
double re = cir.getZC(); //调用内部成员函数
cout << re << endl;
system("pause ");
return 0;
}
2.修饰符
也是三种
public private protect
关于struct与class的区,他们其实差不多,不同点也就是默认权限
struct默认的权限public
class默认的权限private
3.初始化与清理
构造函数与析构函数
构造函数就是初始化的函数;析构函数是对象被释放时调用的函数(通常用来释放内部的其他内存,如堆上的未释放空间)。
3.1 构造函数
3.1.1语法
类名 ()
{
//内容
}
//如:
Circle()//圆的空构造函数
{
}
函数名就是类名,没有返回值,也不写返回值。
可以有参数,可以重载构造函数,程序创建对象时一定会调用构造函数,不用手动调用。
3.1.2 析构函数
~类名 (){}
与构造函数基本一致,不同的是它没有参数。只会在对象销毁的时候哦调用。只需在构造函数前加~
.
4.构造函数
4.1 类型
-
按有无参数分
- 无参
- 有参
-
按构造方式分
-
普通构造
-
拷贝构造
拷贝构造函数自动调用的时机:
- 以传参的方式把对象传给函数形参
- 以函数返回值的形式自动返回拷贝对象
-
const double PI = 3.1415926;
class Circle
{
public:
double m_r;//成员变量习惯上加 m_ 以区分。
//无参构造
Circle() {}
//有参构造
Circle(int r)
{
this->m_r = r;
}
//拷贝构造函数
Circle(const Circle& circle) {
this->m_r = circle.m_r;
}
double getZC() {
return 2 * this->m_r * PI;
}
};
4.2 调用构造函数
三种方法,括号,显式,隐式,分别如下:
const double PI = 3.1415926;
class Circle
{
public:
double m_r;//成员变量习惯上加 m_ 以区分。
//无参构造
Circle() {}
//有参构造
Circle(int r)
{
this->m_r = r;
}
//拷贝构造函数
Circle(const Circle& circle) {
this->m_r = circle.m_r;
}
double getZC() {
return 2 * this->m_r * PI;
}
};
int main()
{
//1.括号
Circle cir11;//无参
cir11.m_r = 11;
Circle cir12(12);//有参
Circle cir13(cir11);//拷贝
//2.显式
Circle cir21 = Circle();
cir21.m_r = 21;
Circle cir22 = Circle(22);
Circle cir23 = Circle(cir21);
//3.隐式
Circle cir32 = 32;
Circle cir33 = cir32;
cout << cir11.m_r << " ";
cout << cir12.m_r << " ";
cout << cir13.m_r << endl;
cout << cir21.m_r << " ";
cout << cir22.m_r << " ";
cout << cir23.m_r << endl;
cout << cir32.m_r << " ";
cout << cir33.m_r << endl;
system("pause");
return 0;
}
注:对于Circle(10);
这行代码,没有用变量接收,属于一个匿名对象。特点:执行完当前行后立刻释放。
4.3默认实现构造函数
默认提供的函数实现的功能
无参构造(空);
有参构造(所有值的赋值);
拷贝构造(所有值的拷贝)(浅拷贝,深拷贝需要自己实现);
不提供默认函数的规则:
- 如果提供有参构造,编译器不再提供无参构造;、
- 如果提供拷贝构造,编译器不再任何构造;
4.4初始化列表
另一种对象初始化方式。
#include <iostream>
using namespace std;
#include<string>
class student
{
public:
string m_name;
int m_age;
string m_gender;
student() :m_name("未知"), m_age(0), m_gender("未知")//默认初始化序列表
{
}
student(string name, int age, string gender) :m_name(name), m_age(age), m_gender(gender) //有参初始化列表
{
}
void print()
{
cout << "name: " << m_name << ",age: " << m_age << ",gender: " << m_gender << endl;
}
};
int main()
{
student stu;
stu.print();
}
5.对象内的成员对象
对象内的一个成员变量是另一个对象,则这个对象称为成员对象。
构造·有成员对象的·对象时,会先去构建自身的成员对象,再去完成构造自己。(构造函数调用的先后顺序)
销毁会先销毁成员变量,再销毁自己。(析构函数调用的先后顺序)
6.静态成员
6.1 静态成员变量
所有对象共享同一内存(共享静态成员变量);
一般可以类内定义,类外初始化。(注:初始化静态成员变量不受权限限制)
用类名访问的方式:int 类名::静态成员变量
6.2 静态成员函数
静态成员函数只能访问静态成员变量。也可以通过类名直接访问。
7.对象的内存
c++会给每个空对象1个字节的空间为了区分,其占用的内存位置
关于对象的内存,(记忆:共享的数据不属于对象的空间,如函数,和静态变量)
class student
{
int m_A;//成员变量,属于类的对象上的空间
static int m_B;//静态成员变量,不属于类的对象上的空间
void func1()// 成员函数,不属于类的对象上的空间
{
}
static void func2()//静态成员函数,不属于类的对象上的空间
{
}
};
结论:只有非静态成员变量才会占用对象的内存。
8.this指针
非静态函数内都有一个this指针,指向调用函数的对象地址。
作用:
- 可以使用
this->成员变量
访问类内的成员变量。 - 可以使用
return *this;
返回调用对象自己,方便链式编程。
c++编写成员变量习惯,成员变量写m_名字
,用于区分
c++中,空指针可以访问成员方法,但是如果成员方法里使用了this或者访问了空指针的内部成员变量,就会出现错误。
可以再成员函数内判断if(this==NULL)return;
提高代码的健壮性
9.const修饰对象成员
9.1 修饰成员函数
const修饰的成员函数称为常函数
特点:常函数不可以修改成员函数的值,除非成员函数声明时加了mutable
关键字。 如mutable int m_A;
9.2 修饰成员变量
const修饰的成员函数称为常变量
特点:常变量不能被修改,只能调用·常函数。
10.friend友元(让其他元素可以访问类内私有的部分)
- 全局函数做友元(好朋友,可以访问私有内容)
可以使全局函数访问类中的私有内容
#include <iostream>
using namespace std;
class Bank_Card
{
friend void function(Bank_Card* bc);//使全局函数void function()可以访问类中的私有内容int password
public:
Bank_Card(int password) //有参构造函数
{
this->m_password = password;
}
private:
int m_password;//私有成员变量
};
void function(Bank_Card* bc)
{
cout << bc->m_password << endl; //可以得到结果123456
}
int main()
{
Bank_Card bc(123456);
function(&bc);
system("pause");
return 0;
}
- 类做友元
可以使另一个类访问该类的私有内容。
#include <iostream>
using namespace std;
class Bank_Card
{
friend class Person;//使Person类可以访问类中的私有内容int password
public:
Bank_Card(int password) //有参构造函数
{
this->m_password = password;
}
private:
int m_password;//私有成员变量
};
class Person
{
public:
void getPassword(Bank_Card* bc)
{
cout << bc->m_password << endl; //可以得到结果123456
}
};
int main()
{
Bank_Card bc(123456);
Person person;
person.getPassword(&bc); //通过person访问Bank_Card中的私有成员password
system("pause");
return 0;
}
- 成员函数做友元
可以使成员函数访问类中的私有内容(很难,因为c是按照顺序从上到下执行的,很多事情需要声明, 要注意顺序)
#include <iostream>
using namespace std;
class Bank_Card; //1.先声明Bank_Card类,让Person类知道有这个类,不至于报错
class Person
{
public:
void getPassword(Bank_Card* bc); //2.再声明Person中的getPassword函数,让Bank_Card知道他的存在
};
class Bank_Card
{
friend void Person::getPassword(Bank_Card* bc);//3.使Person中的getPassword函数可以访问类中的私有内容int password
public:
Bank_Card(int password) //有参构造函数
{
this->m_password = password;
}
private:
int m_password;//私有成员变量
};
void Person::getPassword(Bank_Card* bc)
{
cout << bc->m_password << endl; //4.最后再实现getPassword内容,确保实现的时候已经定义过Bank_Card中的m_password成员变量
}
int main()
{
Bank_Card bc(123456);
Person person;
person.getPassword(&bc);
system("pause");
return 0;
}
11.在类外创建成员函数
类名 :: 函数
和初始化函数的格式一致。
运算符重载
1.加法+运算符
函数的名字可以写成operator+
这样可以实现 +
运算符 的重载,
也可以在类内部的成员变量实现,相当于给类之间的运算增加重载。
本质上还是调用函数,只不过是有一种简写的形式。
1.1 类内重载
#include <iostream>
using namespace std;
class Number {
public:
int m_a;
int m_b;
Number() {}
Number(int a, int b) {
m_a = a;
m_b = b;
}
//加法+的重载,类内
Number operator+ (Number& number) {
return Number(number.m_a + m_a, number.m_b + m_b);
}
};
int main()
{
Number n1(1, 2);
Number n2(3, 4);
Number n3 = n1 + n2;
cout << n3.m_a << endl; //结果为: 4
cout << n3.m_b << endl; //结果为: 6
system("pause");
return 0;
}
1.2函数外的重载
#include <iostream>
using namespace std;
class Number {
public:
int m_a;
int m_b;
Number(){}
Number(int a, int b) {
m_a = a;
m_b = b;
}
};
//类外的重载函数
Number operator+ (Number& n1,Number& n2) {
return Number(n1.m_a + n2.m_a, n1.m_b + n2.m_b);
}
int main()
{
Number n1(1, 2);
Number n2(3, 4);
Number n3 = n1 + n2;
cout << n3.m_a << endl; //结果为: 4
cout << n3.m_b << endl; //结果为: 6
system("pause");
return 0;
}
注:1。内置的数据类型运算是不可更改的。2.不要滥用重载。
2.左移<<运算符重载
一般用来指定自定义类的输出格式,一般不写在类内,即不用成员函数写,而用全局函数如:
ostream& operator << (ostream& cout,Number& n)
{
cout << "number.m_a = " << n.m_a << ",number.m_b = " << n.m_b;
return cout;
}
注意,如果类中的变量是私有的可以使用友元去访问。
3.递增++运算符重载
#include <iostream>
using namespace std;
class Number {
public:
int m_a;
int m_b;
Number(int a, int b) {
m_a = a;
m_b = b;
}
Number(Number& n)
{
this->m_a = n.m_a;
this->m_b = n.m_b;
}
//重载前置++运算符,
Number& operator++()
{
this->m_a++; //实现内容:两个数字都自增
this->m_b++;
return *this; //细节:返回引用实现链式编程
}
//重载后置++运算符,
Number& operator++(int)//占位int,可以认为是后置++的要求写法
{
Number temp(*this); //临时存储,因为后置++,返回值为未操作的值(注意,这样实现不支持链式编程)
this->m_a++; //实现内容:两个数字都自增
this->m_b++;
return *this; //细节:返回引用实现链式编程
}
};
int main()
{
Number n(1, 2);
++n;
cout << n.m_a << endl; //结果为: 2
cout << n.m_b << endl; //结果为: 3
n++;
cout << n.m_a << endl; //结果为: 3
cout << n.m_b << endl; //结果为: 4
system("pause");
return 0;
}
4.赋值运算符=重载
c++的类中实际默认实现有提供4个函数,前三个 构造函数 拷贝构造 析构函数
第四个是=的默认重载,和拷贝函数的默认实现一样。
<span style="color:red">注意:深拷贝时要先判断,源对象在堆中是否有数据,然后再清除,再进行深拷贝。</span>
5.关系运算符== 和!=重载
和前面差不多,返回bool就行。
6.函数调用运算符()重载
因为类似函数调用,所以也叫仿函数
了解:匿名函数对象
下面的Myadd是实现了函数调用
(括号)
运算符重载的类。MyAdd()
为构造对象的函数,后面的(100,100)
为调用的括号()
重载
cout << MyAdd()(100,100) << endl;
继承
1.继承格式
class 子类 : 继承方法 父类
{
//内容
}
//如:
//Base是Son继承的父类
class Son :public Base
{
//内容
}
继承方法有public、protected、private
public:直接继承所有内容,最低访问权限为public(可以继承但是不能访问父类中的private内容)
protected:继承所有内容,最低访问权限为protected(但是不能访问父类中的private内容)
private:继承所有内容,最低访问权限为private(但是不能访问父类中的private内容)
2.继承相关问题
2.1 父类与子类的构造与析构顺序
子类被创建前,一定会先创建父类对象。析构顺序相反。
即 子类的创建与销毁顺序是:【父类构造】-->【子类构造】-->【子类析构】-->【父类析构】
2.2 子类与父类的成员重名
如果重名,直接调用子类的方法,而不是父类。
如果想调用父类的,需要加上父类的作用域,如:
son是一个Son的对象,Base是Son的父类
son.Base::m_A; //这里访问的就是父类Base里的m_A
如果子类中有与父类中的重名函数,则会将父类中的所有相同名字的成员函数全部隐藏(包括重载)。若想调用则需加作用域。
同名静态成员,操作与普通成员一致。
2.3 继承信息的查看工具
继承信息的查看工具3.多继承语法
3.1 格式
class 子类 : 继承方法 父类,继承方法 父类,...
{
//内容
}
//如:
// Base1等 都是Son继承的父类
class Son :public Base1,public Base2,...
{
//内容
}
3.2 问题
3.2.1 重名
父类中出现了重名的内容需要使用作用域区分。
3.2.2 菱形继承(重复继承问题)
设有四个类,Class1,Class2,Class3,Class4
其中Class2和Class3继承Class1 , Class4又继承了Class2和Class3 。
这就是菱形继承,常见的重复继承,Class4等于继承了两次Class1。
解决方案:虚继承 ::virtual
内部实际是继承 指针数据 保证数据只有一份。
正常写代码时,不建议使用多继承的操作,仅仅作为了解。
格式:
class 子类 :virtual 继承方法 父类
{
//内容
}
//如:
//Base是Son继承的父类
class Son :virtual public Base
{
//内容
}
多态
1.概念
子类可以使用父类的名字。
2.多态的问题
2.1 调用的函数是父类的还是子类的
静态多态(早绑定)、一定是调用者的函数
动态多态(晚绑定):看调用者的实际类型所重写的函数(和java一致) (使用虚函数:virtual
)
动态绑定底层实际也是一个虚函数指针。原理:使用函数指针,从父类到子类的继承就是一个不断覆盖函数指针的过程。
纯虚函数
写法 :
virtual void func() = 0; //纯虚函数
如果一个类中有一个纯虚函数,则这个类称为抽象类。(类似于java中的抽象函数与抽象类)
特点:
- 抽象类无法实例化对象;
- 子类如果不重写纯虚函数,则子类也是抽象类。
虚析构和纯虚析构
虚析构和纯虚析构的存在是为了使子类可以释放自己的所有内存。主要是堆上的数据的释放。使用方法和虚函数和纯虚函数基本一致,细节如下:
区别 有纯虚析构无法实例化对象,但是如果是有虚析构可以实例化对象(类内没有其他的虚函数)。
但是纯虚析构一般需要代码实现,可以先声明函数=0;
,再在函数外实现函数体。
文件操作
1.准备
进行文件操作的头文件<fstream>
文件类型
- 文本文件,以ASSCII码存储
- 二进制文件,以二进制存储
操作文件的类型
- 读ifstream
- 写ofstream
- 读写fstream
2.基本操作
-
包含头文件
#include<fstream>
-
创建流对象
流对象
-
打开文件
流对象.open("文件路径",打开方式1|打开方式2);
也可以在创建时直接传参,两步合一步
注:如果是读,要判断文件是否打开成功。
判断方式
流对象.is_open()
,返回值为bool:成功为true,失败为false。
-
写或读
流对象<<
或流对象>>
非二进制的读文件的四种方式
//第一种: char buf[1024] = {0}; while(ifs>>buf) { cout<<buf<<endl; } //第二种 char buf[1024] = {0}; while(ifs.getline(buf,sizeof(buf))) { cout<<buf<<endl; } //第三种 string buf; while(getline(ifs,buf)) { cout<<buf<<endl; } //第四种 char c; while((c=ifs.get())!=EOF) { cout<<c; } cout<<endl;
二进制的读写
流对象.read
流对象.write
-
关闭文件
流对象.close();
补充:
#pragma once //防止头文件重复包含 #include <iostream> //包含输入输出流头文件 using namespace std; //使用标准命名空间