C++学习笔记

1.字符串string

    //c风格
    char char1[20];
    char char2[20] = "cat";
    //c++风格
    string str1;
    string str2 = "dog";
    //复制
    str1 = str2;
    strcpy(char1, char2);
    //添加
    str1 += " yelling";
    strcat(char1, " yelling");
    //长度
    int len1 = str1.size();
    int len2 = strlen(char1);
  • 构造函数

                             

  • 查找
    string str1 = "abcdefgde";
    //1.find:返回该串起始位置,未查到返回-1
    int pos = str1.find("de");//pos = 3 fin从左向右找
    pos = str1.rfind("de");//pos = 7 rfind从右向左找
    //2.replace:将1到3替换为1234
    str1.replace(1, 3, "1234"); //str1 = a1234efgde

2.结构体做参数

#include<iostream>
#include<string>
using namespace std;
 
//学生结构体定义
struct student
{
    //成员列表
    string name;  //姓名
    int age;      //年龄
    int score;    //分数
};
 
//值传递
void printStudent(student stu )
{
    stu.age = 28;
    cout << "子函数中 姓名:" << stu.name << " 年龄: " << stu.age  << " 分数:" << stu.score << endl;
}
 
//地址传递
void printStudent2(student *stu)
{
    stu->age = 28;
    cout << "子函数中 姓名:" << stu->name << " 年龄: " << stu->age  << " 分数:" << stu->score << endl;
}
 
int main() {
 
    student stu = { "张三",18,100};
    //值传递
    printStudent(stu);
    cout << "主函数中 姓名:" << stu.name << " 年龄: " << stu.age << " 分数:" << stu.score << endl;
 
    cout << endl;
 
    //地址传递
    printStudent2(&stu);
    cout << "主函数中 姓名:" << stu.name << " 年龄: " << stu.age  << " 分数:" << stu.score << endl;
 
    // system("pause");
 
    return 0;
}

对于参数为结构体或者复制数据类型时,一般采用地址传递(指针或者引用),因为值传递时会将参数复制一份会占用更多的内存(这里student stu有40字节),而指针仅占8字节
若害怕地址传递导致不必要的修改,可以用const修饰

3.指针和引用

//在x86操作系统下,指针占4字节,在64位操作系统下指针占8字节
//解释:指针即地址,64位下要寻遍整个地址空间(虚拟地址)需要2^64即8字节
    int a = 10;
    int *p = &a;  //指针一定要初始化(有指向),不然变成野指针
    *p = 1000;  //p指向a的地址,*p代表该地址的内容,修改*p相当于修改a
    cout<<"此时的a值被修改为:"<<a<<endl;
    cout<<"sizeof(int *)"<<sizeof(p)<<endl;
//指针常量和常量指针
int a = 10;
int b = 20;
// 指针常量,只能修改内容,指向不可修改
int * const p = &a;
*p = 100;
// 常量指针,只能修改指向,不能修改内容
const int * p2 = &a;
p2 = &b;
// 混合方式,都不可以修改
const int * const p3 = &a;

记忆:指针常量 const修饰的是p,p指向a的地址,由于const则指向不可修改

          常量指针const修饰的是*p,*p是内容,则内容不能修改

//指针和数组
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    int *p = arr;
    cout<<"数组的第一个元素:"<<*p<<endl;
    p++;  //有一种写法是P+4,因为int占4个字节,但有潜在隐患,即不同处理机数据类型可能不一
    cout<<"数组的第二个元素:"<<*p<<endl;

指针还有一个用法是解决修改形参问题,还有一种方式就是使用引用来传递,引用和指针形参都会修改实参

void swap(int *p1 , int *p2)
{
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
} 
  //用指针的方式
   int a = 10; int b = 20; swap(&a,&b);//传地址

void swap(int &a,int &b){
    int tmp = a;
    a = b;
    b = tmp;
} 
  //用引用的方式
    int a = 10,b=20;
    swap(a,b);//通过引用来传递和用指针传递类似

但是需要注意:

1.引用必须要初始化,2.引用一旦初始化后就无法更改(因为引用的本质其实就是:指针常量即-->type* const p)
用引用还可以实现 --->函数引用做返回值
int& test01(){//用来返回静态变量引用
    static int a = 6;
    return a;
}
    
  int& ref = test01(); cout<<"ref:"<<ref<<endl;  //输出6 test01() = 666;//使用引用做返回值,那么函数可以做左值修改 cout<<"ref:"<<ref<<endl;  //输出666

 右值引用:右值引用有一个重要的性质—只能绑定到一个将要销毁的对象。 因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

4.函数重载

需满足三个条件:

1.必须在同一个作用域  2.函数名相同  3.参数类型不同,或个数,或顺序不同
void fun(int &a){
//调用的是变量
}
void fun(const int &a){
//调用的是常量
}
void func(int a){
}
int func(int a){return a;}//错误,无法仅通过返回值重载

 需要注意不能出现二义性,例如:函数中带有默认参数则会导致func(1)即可以调用第一个,又可以调用第二个,此时编译器人傻了不知道你到底要调哪一个,所有重载函数尽量不使用默认参数

 

 

5.类的访问权限

1.public   类内类外皆可访问
2.protected  类内可以,类外不可以   子可以访问父中的内容
3.private    类内可以,类外不可以    子不可以访问父中的内容
struct 默认是public  class 默认是private

protected和private的主要区别在继承中体现的明显一点:

用protected继承时,派生类可以访问类内成员,而使用private继承时,派生类是访问不到类内成员

注意:class中默认private,struct中默认public

 

6.类和类的各种构造函数

类成员的调用顺序是先枝干再整体,销毁顺序是先整体在枝干
比如说我写了一个Person类和Phone类,在person中用phone创建一个phone,那么调用结果如下
class Phone{
    public:
    Phone(string name) :m_PName(name) {//参数列表在构造函数执行前执行
        cout<<"phone"<<endl;
    }
    const string m_PName;
    ~Phone(){
        cout<<"Phone的析构"<<endl;
    }
};

class Person{
    ~Person(){
        cout<<"Person的析构"<<endl;
    }
    Person(){
        cout<<"person"<<endl;
}
    string m_name;
    Phone m_phone;
};    

 

C++编译器默认情况下会自动完成,1.默认构造 2.默认析构 3.默认拷贝
调用规则:1.若重写了有参构造,则编译器不再提供默认构造 2.若重写拷贝构造,则不再提供其他构造
 
一般我们重写构造函数来做初始化,重写析构函数来释放堆区数据,重写拷贝构造来实现深拷贝(下图右)

 

 

 浅拷贝的危害:在析构函数执行时(新开辟的指针指向员有指针的相同空间),导致该堆区数据被重复释放,导致程序奔溃

比如创建一个Person类,并声明一个指针m_height,在构造函数中将其开辟在堆区,此时需要做深拷贝

 

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
    // 无参(默认)构造函数
    Person()
    {
        cout << "无参构造函数!" << endl;
    }
    // 有参构造函数
    Person(int age, int height)
    {
        cout << "有参构造函数!" << endl;
        m_age = age;
        m_height = new int(height);
    }
    // 拷贝构造函数
    Person(const Person &p)
    {
        cout << "拷贝构造函数!" << endl;
        // 如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
        // 所以凡是涉及堆区的数据都需要深拷贝
        m_age = p.m_age;
        // m_height = p.m_height;//浅拷贝写法
        m_height = new int(*p.m_height); // 深拷贝写法
    }
    // 析构函数
    ~Person()
    {
        cout << "析构函数!" << endl;
        if (m_height != NULL)
        {
            delete m_height;
            m_height = NULL;
        }
    }
public:
    int m_age;
    int *m_height;
};

void test01()
{
    Person p1(18, 180);
    Person p2(p1);
    cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height <<"地址:"<<p1.m_height<< endl;
    cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height <<"地址:"<<p2.m_height <<endl;
}

int main()
{
    test01();
    return 0;
}

 

7.static关键字

1.全局静态变量

  在全局变量前加static就变成静态数据(即使是函数内部的静态局部变量)也存放在全局数据区。全局数据区的数据并不会因为函数的退出而释放空间  

  作用域:从定义开始处到文件结尾  初始化:未经初始化的话,自动初始化为0 

如下,我们并未给num显示赋值,但结果为0

#include<iostream>
using namespace std;
static int num;//全局静态变量
int main(){
    cout<<"num: "<<num<<endl;
    system("pause");
    return 0;
}

 2.局部静态变量

  同上也存放在全局数据区

  作用域:局部作用域,但离开作用域后并未销毁,任驻留在内存中  初始化:未经初始化的话,自动初始化为0

如下,在func中声明一个局部静态变量,并未初始化,但两次调用中一次为0(自动初始化),第二次为1,代表该数据值得到了保留(即在全局数据区)

#include<iostream>
using namespace std;
void func(){
    static int num;//声明一个局部静态变量
    cout<<"num:"<<num<<endl;
    num++;
}
int main(){
    func();
    func();return 0;
}

3.静态函数

  在函数返回类型前加static,就被定义为静态函数

  函数默认情况下是extern的,但加static后在其他文件中便不可见,比如上面代码的func加一个static修饰,其他文件中便无法感知

4.类的静态成员

 

 

  静态成员类是类的所有对象中共享的成员,而不是某个对象的成员。

  对多个对象来说,静态数据成员只存储一份,供所有对象共用,且不占用类的大小

如下程序中,我们对a进行赋值,只输出最后一次赋值的结果,这是因为a仅被存储了一份。而非静态成员则是由每个对象各自维护。

#include<iostream>
using namespace std;

class Static_exp{
public:
    static int m_a;//声明多少个都不影响该类的大小,从输出结果也可以看出,他们地址差距很远
    static int m_c;
    int m_b;
    int m_d;
    Static_exp(int a,int b){
        this->m_a = a;
        this->m_b = b;
    }
    void get_a_b(){
        cout<<this->m_a<<" "<<this->m_b<<endl;
    }

};
int Static_exp::m_a=0;//注意一定要初始化
int main(){
    Static_exp s1(10,10),s2(20,20),s3(30,30);
    s1.get_a_b();
    s2.get_a_b();
    s3.get_a_b();
    cout << "该类的大小:" << sizeof(Static_exp) << endl;
    cout << "m_a的地址:" <<&s1.m_a<< endl;
    cout << "m_b的地址:" <<&s1.m_b<< endl;
    return 0;
}

5.类的静态函数

   静态成员函数和静态成员变量一样,都属于类的静态成员,不是对象成员

  因此,对静态成员的引用不需要用对象名

  且1.所有对象共享同一个函数 2.静态成员函数只能访问静态成员变量

 
class Person{
        public:
            //静态成员函数
            static void func(){
                m_A = 100; //静态成员函数可以访问静态成员变量
                // m_B = 200; //错误!!不能访问非静态成员变量
            }
        static int m_A;//静态成员变量
        int m_B;//非静态成员变量
    };

普通的成员函数一般都隐含了一个this指针,this指针指向类的对象本身,因为普通成员函数总是具体的属于某个类的具体对象的。通常情况下,this是缺省的。如函数fn()实际上是this->fn()。但是与普通函数相比,静态成员函数由于不是与任何的对象相联系因此它不具有this指针。从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数。

8.C++内存分区模型

  C++内存模型可以分为:

    1.代码区:二进制代码,由OS进行管理

    2.全局区:进一步细分为静态存储区和常量存储区

        静态存储区:主要存储全局静态变量,局部静态变量,全局变量,以及虚函数表

        常量存储区:全局常量,函数指针,常量数组

    3.栈区:由编译器自动分配释放,存放函数的参数值,局部变量等

    4.堆区:由程序员手动分配和手动释放,程序员未释放在程序运行结束后由OS释放

 

 

 做几个简单的验证

class MyClass {
public:
    int m_c;
    static int m_d;
};

int MyClass::m_d = 0;

const int c_g_a = 10;
int main() {
    
    int a = 10;
    int b = 10;
    static int c = 10;
    const int c_l_a = 20;

    cout << "局部变量地址:" << (int)&a << endl;
    cout << "局部变量地址:" << (int)&b << endl;

    cout << "全局变量地址:" << (int)&m_a << endl;
    cout << "全局变量地址:" << (int)&m_b << endl;

    MyClass mc;
    cout << "MyClass变量地址:" << (int)&mc << endl;
    cout << "MyClass中的m_c:" << (int)&mc.m_c << endl;

    cout << "MyClass中的m_d(静态成员属性):" << (int)&mc.m_d << endl;
    cout << "局部静态变量c:" << (int)&c << endl;

    cout << "const修饰的全局变量a:" << (int)&c_g_a << endl;
    cout << "const修饰的局部变量a:" << (int)&c_l_a << endl;

    system("pause");
    return 0;
}

 

   红色区域(全局区)的地址和黄色区域(栈区)的地址明显有一定的差距,注意两个相邻全局的地址正好相差4个字节,正好对应int变量

栈区数据和堆区数据的释放时机不同,堆区的生命周期由程序员决定,而栈区是当该变量使用结束后便释放,如下

#include<iostream>
using namespace std;
int* func() {
    int a=10;//局部变量存放在栈区,栈区数据在执行完后自动释放
    return &a;
}
int main() {
    int *p = func();
    cout << *p << endl;
    cout << *p << endl;

    system("pause");
    return 0;
}

第一次可以正常输出是编译器做了保留,但也就保留一次,第二次就乱码了。

 

 

和上面堆区的数据做一个对比,我们将func()中的a开辟在堆区再打印便不会有任何问题

#include<iostream>
using namespace std;
int* func() {
    int *a= new int (666);
    return a;
}
int main() {
    int *p = func();
    cout << *p << endl;
    cout << *p << endl;
    cout << *p << endl;

    system("pause");
    return 0;
}

  关于虚函数,虚函数表的问题可以看这篇博客:C++虚函数表剖析 | Leo的博客 (leehao.me)

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。

9.this指针

  作用:1.解决名称冲突,2.返回对象本身用*this  本质:this指针的本质是指针常量,即指向不可修改

#include<iostream>
using namespace std;
class Person_this {
public:
    Person_this(){}
    Person_this(int age) {
        this->age = age;//不用this的话,就成了命名冲突
    }//1.当形参和成员变量同名时,用this指针来区别

    Person_this& PersonAddAge(Person_this &p) {
        this->age += p.age;
        // this指向p2的指针,而*this指向的就是p2的本体
        //返回对象本身
        return *this;
    }
    //常函数
    void show_person() const {
        //加上const修饰的是this指针本来为:Perosn* const this  变为 const Perosn* const this
        //this->age = 100;//报错
      this->m_B = 200;//正确
} int age; mutable int m_B;//加上mutable关键字之后,变为特殊变量,即使在常函数中也可以修改 }; void test06() { const Person_this p;//此时声明的p为一个常对象,常对象是不能修改属性的--->mutable修饰的除外 // 且常对象只能调用常对象 //p.age = 10;//立马报错,const对象只能修改mutable修饰的 p.m_B = 100; p.show_person(); cout << "m_B:" << p.m_B << endl; Person_this p1(10); Person_this p2(10); cout << "age:" << p1.age << endl; // 链式编程思想 p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1); cout << "p2:age:" << p2.age << endl; } int main() { test06(); system("pause"); return 0; }

      返回引用                                     返回值

  如上中的const修饰的常函数一般用于变量输出,防止在该函数中对变量进行修改

  而返回*this即返回对象本身,再结合函数的引用类型可以实现链式编程(上左),即一直调用(如cout后可以追加任意多个<<),但是如果返回值类型不是引用,那么就变为了返回对象的值(上右)

10.友元

  友元主要是用来解决访问不到私有类的问题,分为:

  1.全局函数做友元   friend 函数名();  2.类做友元  friend 类名;  3.成员函数做友元 friend 类名::函数名();

 

#include<iostream>
#include<string>
using namespace std;
class Gay_friend {
public:
    void visit_Person();
};

class Person {
    friend void visit();//让该全局函数做友元
    friend class Gay_friend;//类做友元
    friend void Gay_friend::visit_Person();    //成员函数做友元
public:
    void visit_sittingroom(string name);
private:
    void visit_bedroom(string name);
};

void Person::visit_sittingroom(string name) {
    cout << name << "正在访问客厅..." << endl;
}

void Person::visit_bedroom(string name) {
    cout<<name << "正在访问卧室..." << endl;

}

void Gay_friend::visit_Person(){
    string name = "友元类";
    Person p;
    p.visit_sittingroom(name);
}

void visit() {//全局函数做友元
    string name = "全局函数";
    Person p;
    p.visit_bedroom(name);
}

int main() {
    visit();
    Gay_friend gf;
    gf.visit_Person();
    system("pause");
    return 0;
}

 

 11.运算符重载

  主要解决自定义数据类型的运算操作

#include<iostream>
#include<string>
using namespace std;
class MyInteger {
    friend int operator+(MyInteger &p1, MyInteger &p2);
    friend ostream& operator<<(ostream &cout, MyInteger myint);
public:
    MyInteger(int num, int age) {
        m_num = num;
        m_age = new int(age);
    }
    MyInteger(int num) {
        m_num = num;
    }
    ~MyInteger() {
        if (m_age != NULL) {//若age在堆区开辟数据,则释放堆区内存
            delete m_age;//此时若不做深拷贝则会重复释放同一内存而报错
            m_age = NULL;
        }
    }
    // 重载关系运算符
    bool operator==(MyInteger &p) {
        if (this->m_age == p.m_age && this->m_num == p.m_num) {
            return true;
        }
        return false;
    }
    //重载赋值运算符
    MyInteger& operator=(MyInteger &p) {
        // 应该先判断是否有属性在堆区,有的话先释放干净
        // 即p2=p1,其中的p2在堆区已经有内存了,我们先将他的原有内存释放,再为期开辟一个新的   
        if (m_age != NULL) {
            delete m_age;
            m_age = NULL;
        }
        // 深拷贝
        m_age = new int(*p.m_age);
        // 返回对象自身
        return *this;//这里返回对象本身,是为了实现链式规则
    }
    int *m_age;
private:
    int m_num;

};

//重载加号(双目运算符的重载要在类外实现才行)
int operator+(MyInteger &p1, MyInteger &p2) {
    return p1.m_num + p2.m_num;
}

// 将左移运算符重载
ostream& operator<<(ostream &cout, MyInteger myint) {
    cout << myint.m_num;
    return cout;
}

int main() {

    MyInteger p1(1, 18);
    MyInteger p2(1, 120);
    MyInteger p3(1, 1111);

    if (p1 == p2) { cout << "p1 p2等" << endl; }
    else { cout << "p1 p2不等" << endl; }
    
    cout << "p1+p2的值:" << p1 + p2 << endl;

    p3 = p2 =p1;//重载=号后可以运行
    cout << "p1:" << *p1.m_age << endl;
    cout << "p2:" << *p2.m_age << endl;
    cout << "p3:" << *p3.m_age << endl;
    system("pause");
    return 0; 
}

  其中=号的重载是用来解决浅拷贝的,导致浅拷贝的原因:m_age是一个指针类型,开辟在堆区

  假设:有p1和p2两个对象,此时执行p1=p2,则p1的m_age指向p2的m_age开辟的那块内存空间,这在赋值上确实没错,但在对象销毁时就出错了

  p2执行析构,释放该空间,p1由于也指向这里,则又释放一次,此时程序就崩溃了

 

12.类型转化

c++提供了四种类型转换,分别适用于其他场景

1.static_cast:  

static_cast 用于内置的数据类型、还用于有继承关系的指针或者引用

#include<iostream>
using namespace std;

class Animal{};
class Cat{};
class Dog:public Animal{};

void test01(){
    int a=97;
    char c = static_cast<char>(a);
    cout<<c<<endl;

    Animal *ani=NULL;
    //Cat *cat = static_cast<Cat *>(ani);//没有继承关系无法转换
    Dog *dog = static_cast<Dog *>(ani);//将父类指针转子类
    
    Animal animal;
    Animal &aniref = animal;
    Dog& d = static_cast<Dog&>(aniref);//将父类引用转子类

}

int main(){

    test01();
    system("pause");
    return 0;
}

 

2.reinterpret_cast 

reinterpret_cast 强制类型转换无关指针类型,包括函数指针都可以进行转换

    Animal *animal;
    Cat *cat = reinterpret_cast<Cat*>(animal);
    //animal 和 cat毫无关系也能转化,不像上面还需要继承

3.dynamic_cast:

dynamic_cast 转换具有继承关系的指针或者引用(用法同static_cast),在转换前会进行对象类型检查  和static_cast对比,dynamic_cast会做安全检查

比如子类指针转父类指针是安全的,但父类指针转子类指针就不安全了,主要是担心越界

 

 

 

4.const_cast:  

const_cast  基础数据类型、指针、引用或者对象指针。作用、把const取消掉、

    // 4.const_cast:增加或者消除const
    int a = 10;
    const int& b =a;
    //b=20;//这里是修改不了的
    int &c = const_cast<int &>(b);
    c = 20;
    cout<<a<<endl;
    const int *p1=NULL;
    int *p2 = const_cast<int *>(p1);//消除p1的const给p2
    const int *p4 = const_cast<const int*>(p2);//为p3加上const给p3

13.内联函数

函数调用是需要额外开销的,假如有一些短小简单的函数被频繁调用,会大量消耗栈空间,此时可以将函数定义为内联函数。

   需要注意的点:

   1.在内联函数内不允许使用循环语句和开关语句;
   2.内联函数的定义必须出现在内联函数第一次调用之前;
   3.类结构中所在的类说明内部定义的函数是内联函数。
写法:
inline void test02(int& a,int& b){
    int temp = a;
    a = b;
    b = temp;

 

 

14.封装,继承,多态(C++面向对象三大特性)

1.封装:

  封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,C++ 通过创建来支持封装和数据隐藏(public、protected、private)。

  在C++中 struct和class唯一的区别就在于 默认的访问权限不同

  struct默认是public,而class默认是private

2.继承:

    继承的三种方式--->  1.public继承 2.protected继承 3.private继承  继承后的关系如下

 

     无论是那种继承,基类的私有属性都不可以访问

  继承时的对象创建和销毁顺序:
class Father{
public:
    Father(){
        cout<<"父类构造"<<endl;
    }
    ~Father(){
        cout<<"父类析构"<<endl;
    }
    int m_a;
protected:
    int m_b;
private:
    int m_c;
};

class Son:public Father{
public:
    Son(){
        cout<<"子类构造"<<endl;
    }
    ~Son(){
        cout<<"子类析构"<<endl;
    }
};
  

   现在问题来了?继承之后Son的内存模型是什么样的呢?

  我们sizeof(Son)看一下,发现结果是12,这表明子类将父类中的属性全部继承下来了(无论你是public还是private),但是编译器做了隐藏

  

 

 

 使用VS的开发者命令提示符 输入:cl /d1 reportSingleClassLayoutSon "exp.cpp"   可以看到son的内存模型是这样的

 

  --菱形继承问题:一个子类多继承的俩个父类又同时继承于同一个父类,如图

  

 

  如何解决:使用虚继承

class Animal{
public:
    virtual void speak(){
        cout<<"animal..."<<endl;
    }
    int m_age;
};

class Sheep:virtual public Animal{

};
class Tuo:virtual public Animal{

};
class SheepTuo:public Sheep,public Tuo{

};
void test17(){
    SheepTuo st;
    st.Sheep::m_age = 18;
    st.Tuo::m_age = 200;
    // 这种写法的问题是重复继承了,这时应该用虚继承
    cout<<st.Sheep::m_age<<endl;
    cout<<st.Tuo::m_age<<endl;
    // 此时内存中仅存在一份m_age数据,两个父类通过vbptr指向子类中的该属性
    cout<<st.m_age<<endl;
    // 此时所有输出都为200,因为实际上大家都是一个变量,即后面的赋值会覆盖前面的赋值

}

同上使用开发者命令提示符查看SheepTuo,可以看到它分别继承了一个来自Sheep的vbptr和一个来自Tuo的vbptr,即实际上继承的是指针

  vbptr: virtual base pointer 虚基类指针

  vftable:  virtual function table 虚函数表

3.多态:

  C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

  多态的条件:1.有继承关系,2.子类重写父类中的虚函数

    1.编译时多态-->通过函数重载  -->早绑定  -->编译阶段确定函数地址

    2.运行时多态-->通过虚函数  -->晚绑定  -->运行阶段确定函数地址

  比如:

class Base_19{
    public:
    virtual void func()=0;//声明为纯虚函数
    // 1.无法实例化对象
    // 2.抽象类的子类,必须重写父类中的纯虚函数,不然也无法实例化对象
};
class Son_19 : public Base_19{
    public:
    virtual void func(){
        cout<<"func()"<<endl;
    }
};
void test19(){
    // 用多态技术调用   父类指针或者引用指向子类对象
    Base_19 * base = new Son_19;
    base->func();
}

  虚析构:解决多态使用时,如果子类有属性开辟在堆区,那么父类指针在释放时无法调用到子类的析构代码。

  纯虚析构:和虚析构的区别在于,纯虚析构属于抽象类,无法实例化对象

有点抽象,直接看例子就懂了,如下声明了一个虚析构,程序可以正常运行和调用,但假如去掉析构的virtual结果如下图

class Animal_21{
    public:
    Animal_21(){
        cout<<"animal--构造"<<endl;
    }
    virtual ~Animal_21(){  //声明一个虚析构
        cout<<"animal--析构"<<endl;
    }
    virtual void speak()=0;//声明一个纯虚函数
};

class Cat_21 :public Animal_21{
    public:
    Cat_21(string name){
        m_name = new string(name);
        cout<<"Cat_21--构造"<<endl;
    }
     ~Cat_21(){
        cout<<"Cat_21--析构"<<endl;
        if(m_name!=NULL){//m_name开辟在堆区,
            delete m_name;
            m_name = NULL;
        }
    }
    virtual void speak(){
        cout<<"Cat在说话"<<endl;
    }
    string *m_name;
};
void test21(){
    Animal_21 *animal = new Cat_21("tom");//多态的写法:父类指针指向子类对象
    animal->speak();
    delete animal;
    // 若不写虚析构则子类的析构函数将不会被调用
}

 

 最大的问题是还没等cat的析构调用,程序就已经结束了,然而cat中的m_name是开辟在堆区了,造成了内存泄漏

 

15.C++文件操作

 

 

 

 

 

例:

// 1.包含头文件#include<fstream>
    // 2.创建文件流
    ofstream write_file;
    // 3.指定打开方式和位置
    write_file.open("test.txt",ios::out);//指定以写的方式打开
    // 4.写入数据
    write_file<<"1.this 是 a test"<<endl;
    write_file<<"2.this 是 a test"<<endl;
    write_file<<"3.this 是 a test"<<endl;
    // 5.关闭文件
    write_file.close();
    // 读文件
    ifstream read_file;
    read_file.open("test.txt",ios::in);
    if(!read_file.is_open()){
        cout<<"打开失败"<<endl;
    }else{
        // 读数据的3种方式
        // 1.
        // char buf[1024] = {0};
        // while(read_file>>buf){
        //     cout<<buf<<endl;
        //     //这种方式会导致每碰到一个空格,回车就会换行
        // }
        // 2.
        // char buf[1024] = {0};
        // while (read_file.getline(buf,sizeof(buf)))
        // {   
        //     cout<<buf<<endl;
        //     //这种方式的好处就是源文件什么时候换行,读到的就什么时候换行
        // }
        // 3.
        string buf;
        while (getline(read_file,buf))
        {
            cout<<buf<<endl;
        }        
    }
    read_file.close();

 

#include<iostream>
#include<string>
#include<fstream>
using namespace std;
class Person{
    public:
        string name;
        int age;
};
int main(){
    //写二进制数据
    ofstream ofs;
    ofs.open("test.txt", ios::out | ios::binary);
    Person p = {"张三", 18};
    ofs.write((const char *)&p,sizeof(Person));

    //读二进制数据
    ifstream ifs;
    ifs.open("test.txt", ios::in | ios::binary);
    if(!ifs.is_open())  cout<<"打开失败"<<endl;
    else{
        ifs.read((char *)&p, sizeof(Person));
        cout << p.name<<" "<<p.age << endl;
        } 
    return 0;
}

 

 

 

16.模板

1.函数模板:

    声明方式:template<typename T>  或者template<class T>

函数模板的调用规则:

  1.模板和普通函数都可以调用时,优先调用普通函数

void myprint(int a,int b){
    cout<<"normal..."<<endl;
}

template<class T>
void myprint(T a, T b){
    cout<<"Template"<<endl;
}
  
  myprint(a,b);//优先调用普通函数

  2.可以通过空模板参数列表 强制调用模板

    //强制调用函数模板
    myprint<>(a,b);
  3.模板可以重载
    //重载
    myprint<>(a,b,c);
  4.若函数模板可以更好匹配,优先调用模板
    //当函数模板能更好的匹配时,优先调用函数模板
    //编译器自动推到
    char d = 'd';
    char e = 'e';
    myprint(d,e);//因为普通函数参数是int,此时函数模板更好的匹配了

2.类模板

  类模板中的成员函数只有在调用的时候才去创建,使用类模板时需要指定类型  与函数模板的区别-->函数模板可以自动推导

声明:

template<typename type1=string,typename type2=int>//类模板中有默认参数,没指定的话就是默认类型
class Person{
public:
    Person_06(type1 name,type2 age){
        this->name = name;
        this->age = age;}
    type1 name;
    type2 age;
    void show_Person(){
        cout<<"name:"<<this->name<<" age:"<<this->age<<endl;}
};

在调用类模板成员函数的时候必须指定传入数据类型

//第一种写法指定传入类型
void show(Person<string,int> p){
    p.show_Person();
}

// 第二种写法参数模板化
template<class T1,class T2>
void show(Person<T1,T2> &p){
    p.show_Person();
}

// 3.整个类模板化
template <class T>
void show(T &p){
    p.show_Person();
}

 

  类模板与继承
  子类继承的父类是一个模板时,需要指定出父类中T的类型,如果要想灵活指定出父类中T的类型,子类也需要变为模板
template <class T>
class Base{  //父类为一个类模板
    public:
    T id;
};

template <class T1,class T2>
class Son:public Base<T2>{  //为了子类的灵活性,将子类也定义为一个类模板
public:
    Son(T1 name,T2 id){
        this->name = name;
        this->id = id;
    }
    T1 name;
};

 

参考资料:

[1] C++ 中的 static - 知乎 (zhihu.com)

[2] C++对象模型详解 - RunningSnail - 博客园 (cnblogs.com)

[3] (20条消息) 浅谈C++内存模型_PurpleDream的博客-CSDN博客_c++内存模型

[4] 黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难_哔哩哔哩_bilibili

posted @ 2022-02-05 23:47  TrueDZ  阅读(92)  评论(0编辑  收藏  举报