1、内存的分区模型 (1)代码区:书写的所有代码(二进制形式),由操作系统管理 (2)全局区:放全局变量(在函数体外的变量)、静态变量(static)和常量(const)(包括字符串常量,双引号括起来的部分取其地址:&“hello”、全局常量) (3)栈顶:编译器管理(自动分配和释放),存参数值、局部变量(函数中的变量,包括main函数,而且局部常量也是放这)等 (4)堆区:有程序员管理,若程序员不释放,则在程序结束的时候,系统自动回收
2、内存四区的意义:不同区域存放的数据,赋予不同的生命周期,可灵活编程 3、程序编译后运行前,生成了exe文件,这时只分为两区:(所在的地址块不同,可取址看其区别) (1)代码区 ①共享(有些程序可多次执行exe),即频繁执行的程序,只在内存中一份代码即可 ②只读:防止程序意外修改他的指令 (2)全局区:该区域的数据,在程序结束后由操作系统释放
4、程序运行后: (1)栈区:由编译器自动分配释放,存放函数的参数值,局部变量 ①注意:不要返回局部变量的地址(一般是返回值),栈区开辟的数据由编译器自动释放 1)因为局部变量存放在栈区,栈区的数据在函数执行完之后自动释放(会乱码) 2)不过第一次可以打印正确地数字,是因编译器做了次保留,第二次就错了
(2)堆区:由程序员分配释放,若程序员不释放,在程序结束是有操作系统回收 ①C++中用new在堆区开辟内存(int* p = new int(10);) 1)//new 返回的是该类型的指针 2)int* arr = new int[10];//注:这里是方括号,里面的是表示数组的大小,若不是方括号,则该框中的值为要保存的值 ②用堆区,可以返回局部变量的地址,这样,这部分地址在退出函数时,则还没有释放 ③释放方法:delete 1)delete p;//释放普通类型的数据 2)delete[] arr;//释放数组的时候,要加[]才行
5、引用: (1)本质:给一变量起别名,一起操控同一块内容 (2)注意事项: ①引用必须初始化 ②初始化后,则无法再修改(即只能做一个变量的别名)
(3)引用做函数参数:(引用传递:C++推荐使用这种方式) ①可通过形参修改实参,从而替代指针,简化指针需要的操作(用指针做形参时,调用函数所用变量的前面,还需加取地址符号&) ②总结:函数的参数传递 1)值传递 2)指针传递 ③引用传递void test1(int &a,int &b){}(在函数的定义时设置引用就可以了) 1)效果和指针传递的效果一样,但比指针传递简单
(4)引用做函数的返回值 int& test02(){} ①不要做局部变量的返回值(因为这部分内存在该函数执行完之后,会释放掉,返回的别名也就没啥意义了) ②如果函数的返回值为引用,那么,函数的调用可以作为左值(即等号的左边) 1)test02() = 1000;,因为返回的是引用类型,是一变量名,这样的话,就是在给该变量做赋值操作,修改了该引用指向的内存空间的值
(5)引用的本质是一个指针常量,因而指向的对象不可改,但里面的值是可以改的 int a = 10; int& ref = a;//这里其实会自动转化为int* const ref=&a; ① ref = 20;//这里编译器内部自动转化为*ref=20;
(6)常量引用:主要用来修饰形参,防止误操作 void showData(const int &a) {} ①使得在该函数体中无法修改该值,同时,在其他函数中(如main函数)中确实可以改的 //int& ref = 10;//直接将引用指向自变量的话,会报错, //因为引用必须指向一块合法的内存空间 const int& ref = 10;//在引用前面加上const就可以 //这里加上之后,编译器会进行相应的修改:int temp=10; const int & ref=temp; ②//但这时,引用就只能是只读的状态了,不可修改
6、函数的提高 (1)函数的默认参数(有默认值的形参,要放到最右边) ①调用函数的时候,若没有传对应参数的值,则用默认参数 ②如果函数的声明有了默认参数,则函数实现(定义)就不能有默认参数了(否则会重定义的了默认参数,运行时出错) ③函数声明和定义部分,只能有一个位置有默认值
(2)函数的占位参数: ①形参只写了个类型在那里,没写变量名,但调用的时候,必须传相应类型的参数 ②目前阶段的占位参数,还用不到,后面再讲 ③占位参数,可以是默认参数
(3)函数重载 ①需满足的条件: 1)函数都在同一个作用域下 2)函数名称相同 3)函数参数类型不同、或个数不同、或顺序不同 ②注意事项:函数的返回值不可以作为函数重载的条件 ③引用作为重载的条件时: 1)void fun(int& a) fun(b);以变量a 进行调用的时候,调用的是第一个,相当于给引用赋值:int & a=b; 2)void fun(const int& a) fun(10),直接以常量进行调用的话,调用的是第二个,效果为,int temp=10;const int &a=temp; ④函数重载遇到默认参数 1)void test(int a, int b = 10) 2)void test(int a) 3)test(a);只有一个参数调用的时候,则会报错,因为出现二义性,两者都可以调用,test(a, b);故只能用两参数,调用含默认参数的部分
7、类和对象(封装、继承、多态) (1)三种访问权限 ①Public: 类内可以访问,类外(即用具体对象)也可以访问 ②Protected: 类内可以访问,类外不可以访问(继承时,子类可以访问父类该类型的属性) ③Private 类内可以访问,类外不可以访问(子类不可访问)
(2)Struct 和class的区别(两者没有明显的区别,主要是默认权限) ①Struct默认权限是public ②Class默认权限是private
(3)一般成员属性都设置为私有 ①优点: 1)可以控制成员属性的读写权限(即设置验证或判断关卡) a.若只读,则只提供读的接口函数,不提供写的接口即可 2)对于写程序,在写入之前,还可以检测数据的有效性(即判断数据是否合理) ②注:void setw(int w){} 给属性设置值的时候,若形参的名字和属性的名字一样,则用this->w=w; 若名字不同,则可以直接赋值就可以了
(4)两个同一类的对象,赋值的时,直接等就可以了 (5)(#pragma once防止头文件重复包含) (6)函数的声明和实现分开的时候,函数实现时int Point::getX(){ return x;}中的Point::是类名,作用是对该函数加入作用域
(7)构造函数(初始化,创建对象时自动调用)和析构函数(清理,保证安全) ①程序员不写,则系统有空实现(也即没设置内容),不过写了也是系统自动调用的,只会调用一次 ②构造函数可通过参数进行重载(析构函数则不能有参数,也即不能重载,销毁前自动调用) ③按有无参数分类 1)无参构造(默认构造) 2)有参构造(给属性值初始化) ④按类型分类: 1)普通构造函数 2)拷贝构造函数Person(const Person& p) ()//拷贝构造函数,防止修改,故用const,一般用引用传参,节省空间 ⑤调用有参的构造函数 1)括号法 Person p1(10); a.注意调用无参构造函数创建对象的时候,不要在后面加括号(如Person p1();),因会被编译器视为函数的声明而不会创建类的对象 2)显式法 Person p1=Person(10); a.单独Person(10);则是匿名对象,其特点为:当前行结束后,马上析构,因为没有名字,后面无法利用,故直接销毁 b.注意:不要利用拷贝构造初始化匿名对象:Person (p1); 因为它会被认为Person (p1)相当于Person p1; 而p1在前面已经创建了,会报错重定义 3)隐式转换法 Person p1= 10; 编译器会默认转化为:Person p1= Person(10)
(8)拷贝构造函数调用的时机: ①使用已经创建好的对象初始化一个新的对象(区别于创建的时候的等号) ②值传递的方式传参 ③以值的方式返回局部对象
(9)构造函数的调用规则 ①创建一个类的时候,系统会至少给类添加3个类 1)默认构造函数 2)默认析构函数 3)默认拷贝构造函数(直接对属性值复制) ②若自定义了有参构造函数,则编译器不会提供默认构造函数(无参),但提供拷贝构造 ③若自定义了拷贝构造,则不提供其他构造函数(包括默认构造函数) 1)也即若自定义了拷贝构造函数,而不写其他普通的构造函数,则会导致无法构建对象
(10)深拷贝和浅拷贝 ①浅拷贝(编译器提供的,直接赋值): 1)问题:堆区内存重复释放(指针部分),浅拷贝问题由深拷贝解决(重新开辟一块空间) 2)问题概述:也即有部分属性要存放到堆区时(先进后出)(int *m_height;//把身高整到堆区)(构造函数中:m_height = new int(height);),若直接用编译器提供的浅拷贝,则在Person p2(p1);利用p1赋值的时候,直接是m_height=p1.m_height;由于m_height值是地址,故两者最终指向的是同一块地址;由于new创建的是堆区数据,所以,程序员需要在析构函数中手动释放 if (m_height!=NULL) {delete m_height;//程序员要手动释放堆区的代码 m_height = NULL;}, 故需调用delete m_height;但由于是两个对象p1 p2,故会调用两次,但两 者指向的是同一个空间,释放两次,则会报错 ②深拷贝(堆区的数据重新申请内存) Person::Person(const Person& p) { m_Age = p.m_Age; m_height = new int(*p.m_height);} 这里p.m_height是指针,故需解指针, 用该值在堆区重新开辟一块空间
(11)初始化列表:用于初始化类的属性 ①Person(int a,int b,int c):m_a(a),m_b(b),m_c?{…},替代构造函数,注意冒号的位置 (12)类对象A作为另一个类B的成员(得先定义A,才能定义另一个类B) ①构造时,先构造A再构造B ②析构的时候,则相反
(13)类中成员变量和成员函数分开存储(而且非静态变量才属于对象上面的)
class Person
{
int m_A;
static int m_B;
void func(){}
static void fun2() {};
}
class Person{};
void test()
{
Person p;
cout << "size of=" << sizeof(p) << endl;
}
(14)this指针:对象指针,隐含在每个非静态成员函数内,不需定义,直接使用 ①用途 1)函数的形参和属性同名时 2)在类的非静态成员函数中,返回对象本身,可使用return *this;
Person2& personAdd( Person2& p) {
this->age += p.age;
return *this;
}
Person2 p1(10);
Person2 p2(20);
p2.personAdd(p1).personAdd(p1).personAdd(p1);
(15)Const 修饰成员函数(常函数):常:也即只读 ①常函数内不可以修改成员属性 ②如真的要修改,则需在成员属性上加关键字mutable
void showPerson()const {
this->m_b = 100;
}
int m_A;
mutable int m_b;
(16)声明对象前加const,称为常对象 ①常对象只能调用常函数
const Person4 p;
(17)友元(friend) ①目的:让一个函数或者类访问另一个类的私有成员 ②友元的三种实现 1)全局函数做友元
friend void goodGay(Building& building);
2)类做友元
friend class GoodGay;
3)成员函数做友元
friend void GoodGay::test();
(18)注意:在类的使用过程中,若B类中使用到类A,需在B类前面先声明类A,在类B后面再实现类A(当然,类A在类B前面实现也可) ①要注意:类B成员函数要类外实现,并放在类A的实现的后面,这样才不会报错
class Building;
class GoodGay
{
public:
GoodGay();
~GoodGay();
Building* building;
};
class Building
{
public:
Building();
~Building();
string m_SittingRoom;
string m_BedRoom;
};
8、运算符重载 (1)加号运算符重载(将函数名改为编译器的加号名operator+即可) ①通过成员函数重载+号
Person Person::operator+(Person& p)
{
Person temp;
temp.m_A = m_A + p.m_A;
temp.m_B = m_B + p.m_B;
return temp;
}
②通过全局函数来重载+号
Person operator+(Person& p1, Person& p2) {
Person temp;
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
③调用的实质:
Person p3 = p1 + p2;
cout << p3.m_A << " " << p3.m_B << endl;
④注意: 1)不可重载已有数据类型的符号运算方法,也即对内置的数据类型的表达式的运算符是不可改变的 2)当然,也不要滥用运算符重载,即别把人家加号,在内部做成了减号 (2)左移运算符重载<< ①成员函数重载operator ②全局函数重载
void operator<<(ostream& cout)
{
}
ostream& operator<<(ostream& cout,Person &p1)
{
cout << "m_A=" << p1.m_A << " m_B=" << p1.m_B;
return cout;
}
③注:如果属性是私有属性,则可以将该重载函数设置为类的友元函数 (3)自增运算符重载++(也即对自定义的类起作用) ①前置++
MyInteger& operator++() {
m_Num++;
return *this;
}
②后置++
MyInteger operator++(int) {
MyInteger temp = *this;
m_Num++;
return temp;
}
③注:对于后置++或后置–,其输出的左移运算符重载,传入的对象不能是引用,若是引用,需在前面加const(因为myint++的返回值是一个const限定的右值)
ostream& operator<<(ostream& cout,const MyInteger &myint)
{
cout << myint.m_Num;
return cout;
}
void test() {
MyInteger myint(10);
cout <<myint-- << endl;
}
(4)注:c++编译器至少给一个类添加4个函数 ①默认构造函数 ②默认析构函数 ③默认拷贝函数person p2(p1);即创建类对象的时候使用 ④赋值运算符operator=,对属性进行值拷贝(只是浅拷贝,若有属性指向堆区,则有深浅拷贝的问题)
(5)赋值运算符重载=
Person& operator=(Person& p)
{
if (m_Age!=NULL)
{
delete m_Age;
}
m_Age = new int(*p.m_Age);
return *this;
}
}
(6)关系运算符重载 ①==
bool operator==(Person& p) {
if (m_Name==p.m_Name&&m_Age==p.m_Age)
{
return true;
}
return false;
}
②!=
bool operator!=(Person& p) {
if (m_Name == p.m_Name && m_Age == p.m_Age)
{
return false;
}
return true;
}
(7)函数调用重载() ①函数重载的调用,像普通函数的调用,所以又叫作仿函数,其应用非常灵活,形式不固定,具体实现的功能由其参数和函数体的内容决定 ②用于打印字符串
class Myprint
{
public:
void operator()(string test) {
cout << test << endl;
}
};
void test() {
Myprint myprint;
myprint("hello,word");
}
③用于打印字符串
class AddData
{
public:
int operator()(int a, int b) {
return a + b;
};
};
void test1() {
AddData add;
cout << add(1, 2) << endl;
cout << AddData()(2, 4) << endl;
}
9、继承: (1)继承的书写格式Class python :public BasePage{};//这里的public是继承方式 (2)继承的好处:减少重复的代码 (3)分类 ①公有继承:父类中是相关属性是什么权限,在子类中也是什么权限,只是子类中不可访问父类中私有属性(相当于父类中的隐私的东西) ②保护继承:父类中的public属性,继承下来都是protected属性,父类中的私有属性一样不可访问 ③私有继承:父类中的public、protected属性都变为private属性,同样父类的private属性不可访问
(4)注意:子类在继承的时候,除了静态成员属性外,其他的属性都继承,其中父类中的private属性只是被编译器隐藏了,但是有继承了
(5)继承中,创建子类对象时,先构造父类再构造子类,析构子类对象时,是先析构子类,再析构父类 ①也即析构顺序和构造顺序是相反的 (6)同名成员处理(即父类和子类中有同名的属性或同名的函数) ①子类对象可以直接访问到子类中同名成员 ②子类对象加作用域(即父类名::)访问到父类中的同名成员 ③当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数(包括同名的重载函数),也是加作用域方可访问
void test()
{
son s;
cout << "调用子类中的同名成员"<<s.m_A << endl;
cout <<"调用父类中的同名成员"<< s.Base::m_A << endl;
}
(7)静态同名成员处理(即父类和子类中有同名的属性或同名的函数,且是静态的) ①处理方式和上述非静态成员一样,不过它有两种 1)通过对象访问 2)通过类名访问
void test()
{
son s;
cout << "调用子类中的同名成员"<<s.m_A << endl;
cout <<"调用父类中的同名成员"<< s.Base::m_A << endl;
cout << "调用子类中的同名成员"<<son::m_A << endl;
cout <<"调用父类中的同名成员"<< son::Base::m_A << endl;
cout <<"调用父类中的同名成员"<<Base::m_A << endl;
}
(8)多继承语法 ①c++支持多继承,但在实际开发中不建议使用多继承,因为多个父类中可能会有同名成员,为区别,在调用的时候需要加作用域
void test()
{
son s;
cout << "继承base1 的m_A:" << s.Base1::m_A << endl;
cout << "继承base2的m_A:" << s.Base2::m_A<< endl;
}
(9)菱形继承(两个父类拥有相同的数据,需要加作用域区分) ①该继承导致两个父类的相同的数据有两份,导致资源浪费 ②利用虚继承来解决该问题 1)(在父类(菱形的中间层)继承的前面加virtual 即virtual public Animal),这样,其子类多继承的时候,两个父类中相同的数据就留一份,这样可以使用子类对象直接访问,而不用父类作用域了,因为没有了二义性 2)父类的父类(也即爷爷)叫虚基类
class Animal
{
public:
Animal() {
m_A = 100;
};
int m_A;
};
class sheep:virtual public Animal{};
class tuo :virtual public Animal{};
class sheeptuo:public sheep,public tuo
{};
void test()
{
sheeptuo s;
cout << s.sheep::m_A << endl;
cout << s.tuo::m_A << endl;
cout << s.m_A << endl ;
}
3)使用虚继承后,子类通过父类继承的爷爷类的元素中,其实只是继承了个虚基类指针(vbptr),其指向虚基类列表(vbtable),在虚基类列表中,只存一份该数据
10、多态 (c++开发提倡利用多态设计程序架构,因为多态的优点多) (1)多态的基本语法(动态多态) ①使用多态满足的条件 1)有继承关系 2)子类重写父类的虚函数(注意不是重载,重写是函数参数都一样) ②多态的使用 1)父类的指针或引用 执行子类的对象 2)即参数中写父类的指针或引用,传实参的时候,传的是子类的对象
class Animal
{
public:
virtual void speak() {
cout << "animal 的 speak()" << endl;
}
};
class sheep:public Animal
{
public:
void speak() {
cout << "sheep 的 speak()" << endl;
}
};
void dospeak(Animal& animal) {
animal.speak();
}
void test() {
sheep s;
dospeak(s);
}
3)原理分析 a.对于animal类而言,若不加virtual,其整个类的大小为1字节,若加上之后,则为4字节,为虚函数指针(vfptr),指向虚函数表(vftable),虚函数表中记录的是&animal::speak b.对于sheep类而言,由于继承并重写了speak函数,所以对应的虚函数指针指向的虚函数表中,记录的内容是&sheep::speak c.在调用的时候,父类指针或引用指向的子类对象的时候,发生多态(这时候调用的是子类的speak函数)
(2)多态的优点 ①代码组织结构清晰 ②可读性强(和结构清晰有关) ③利于前期和后期的扩展和维护(即添加新功能的时候,可遵循开闭原则)
class AbstractCaculate
{
public:
virtual int getresult()
{
return 0;
}
int m_A;
int m_B;
};
class add:public AbstractCaculate
{
public:
virtual int getresult()
{
return m_A + m_B;
}
};
class sub :public AbstractCaculate
{
public:
virtual int getresult()
{
return m_A - m_B;
}
};
void test1() {
AbstractCaculate* abc = new add;
abc->m_A = 60;
abc->m_B = 50;
cout <<"加法:"<< abc->getresult() << endl;
delete abc;
abc = new sub;
abc->m_A = 60;
abc->m_B = 50;
cout << "减法" << abc->getresult() << endl;
delete abc;
}
(3)纯虚函数和抽象类 ①多态中,父类的虚函数的实现是没意义的,一般都是调用子类重写的内容 ②故可将该虚函数写为纯虚函数 virtual void spead() = 0; 而含有纯虚函数的类则叫抽象类 1)作用:使得子类必须重写该纯虚函数,否则,无法实例化对象 ③抽象类的特点 1)无法实例化对象 2)子类必须重写抽象类中的虚函数,否则也属于抽象类,即无法实例化子类的对象,创建子类对象的时候会报错
(4)虚析构和纯虚析构 ①应用场景:多态+子类有属性开辟到堆区 ②解决的问题:子类中有属性开辟到堆区的时候,父类指针在释放时无法调用到子类的析构代码,会导致子类内存泄漏(也即多态使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码) ③解决方式:将父类中的析构函数改为虚析构或纯虚析构 ④虚析构:直接在析构函数前面加virtual即可 virtual~Animals(); ⑤纯虚析构:在虚构函数前面加virtual,然后后面=0,即virtual~Animals()=0; 1)但这里只是声明他是纯虚析构函数,也即声明他是抽象类,不能创建其对应的对象 2)由于析构的时候,也要调用父类的析构函数(即释放父类本身),所以,还要在类外实现该纯虚析构函数,否则会报错
void test() {
Animals* ani = new Cat;
ani->speak();
delete ani;
}
11、文件操作 (1)应用场景:程序运行时产生的数据都是临时数据,一旦运行结束都会被释放,所以为了保存这些数据,需要保存到文件中 (2)文件的类型 ①文本文件:即以ASCLL码的形式保存,用户直接看就知道写了啥 ②二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它(虽然并没有进行加密) (3)操作文件的三大类 ①Ofstream 写操作 ②Ifstream 读操作 ③fstream 读写操作
(4)写文件步骤
#include<fstream>
void test()
{
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "前路漫漫雨纷纷" << endl;
ofs << "谁在痴痴等" << endl;
ofs.close();
}
(5)文件打开方式: ios::in 为读文件而打开 ios::out 为写文件而打开 ios::ate 初始位置:文件尾 ios::app 追加方式写文件(也即在文件尾) ios::trunc 如果文件存在先删除,再创建 ios::binary 二进制方式
(6)读文件操作
void test() {
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open()) {
cout << "打开文件失败" << endl;
return;
}
char c;
while ((c=ifs.get())!=EOF)
{
cout << c;
}
ifs.close();
}
(7)二进制写文件操作
#include<fstream>
class Person
{
public:
Person(string name,int age) {
m_Name = name;
m_age = age;
}
string m_Name;
int m_age;
};
void test()
{
ofstream ofs;
Person per("小小", 12);
ofs.open("person.txt", ios::out | ios::binary);
ofs.write((const char*)&per, sizeof(per));
ofs.close()
}
(8)二进制读文件
#include<fstream>
class Person
{
public:
char m_Name[64];
int m_age;
};
void test()
{
ifstream ifs;
ifs.open("person.txt", ios::in | ios::binary);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}
Person per;
ifs.read((char*)&per, sizeof(per));
cout << "姓名:" << per.m_Name << ",年龄:" << per.m_age << endl;
ifs.close();
}
|