Bootstrap

C++基础语法二

1.内存分区模型

C++程序在执行时,将内存大方向划分为4个区域。

在程序编译后,生成exe可执行程序,未执行该程序前分为代码区和全局区两个区域

1.1 代码区

存放函数体的二进制代码,由操作系统进行管理

存放CPU执行的机器指令

代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

代码区是只读的,只读的原因是防止程序以外的修改了它的指令

1.2 全局区

存放全局变量和静态变量(static)及常量(字符串常量,const 修饰的全局变量)

注:局部常量(即const修饰的局部变量不在全局区)

该区域的数据在程序结束后由操作系统释放

1.3 栈区

由编译器自动分配释放,存放函数的参数值,局部变量等(形参数据也会分到栈区)

注:不要返回局部变量的地址 

1.4 堆区

由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

在C++中利用 new 在堆区开辟内存

指针本质上也是局部变量,放在栈上,指针保存的数据放在堆区

2.new运算符 

利用 new 操作符在堆区开辟数据;释放时手动释放,利用操作符 delete

语法: new  数据类型

利用new创建的数据,会返回该数据对应的类型的指针

创建:int * p = new int (10);

释放:delete p;

在堆区创建与释放数组

int * p = new int [10];

delete[] p; //需要说明要释放的是数组,加 [ ]

3.引用

作用:给变量起别名

语法: 数据类型 &别名 = 原名

本质:引用的本质在C++内部实现是一个指针常量

{ int a =10;

int &ref = a; // 自动转换为 int * const ref = &a;  指针常量,指针指向不可改,也说明为什么引用不可更改

ref = 20; // 内部发现ref是引用,自动帮我们转换为 *ref =  20;  解引用 } 

注意:引用必须初始化,且在初始化后,不可发生改变

3.1 引用做函数参数

作用:函数传参时,可以利用引用的技术让形参修饰实参

优点:可以简化指针修改实参

引用传递和地址传递都可以让形参修饰实参,值传递时形参不会修饰实参。

void swap(int &a, int &b) {

    int temp = a;
    a = b;
    b = temp;
}// 在main中可以实现a,b值的转换

3.2 引用做函数的返回值

注意:不要返回局部变量的引用(可能第一次的结果正确,因为编译器做了保留;但在运行一次结果就会错误,因为局部变量的内存已经释放)

用法:函数调用作为左值

用法示例:

int& test() {

    static int a = 10;
    return a; // 返回静态变量引用  静态变量存放在全局区
}

int main() {

    int& ref = test();
    cout << ref << endl; // 10
    test() = 1000;
    cout << ref << endl; // 1000   如果函数的返回值是引用,这个函数调用可以作为左值
    system("pause");
    return 0;
}

3.3 常量引用

作用:常量引用主要用来修饰形参,防止误操作

在函数形参列表中,可以加const修饰,防止形参改变实参

{ int a =10;

 int &ref = 10; // 报错,引用必须引一块合法的内存空间 }

{int a =10;

const int &ref =10; //正确 ,加上const后,编译器将代码修改为 int temp=10; int &ref  = temp;}

4. 函数高级

4.1 函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的

语法: 返回值类型 函数名(参数 = 默认值)

注意事项:

  • 如果我们自己传入数据,就用自己的数据;如果没有,就用默认值
  • 如果某个位置已经有了默认参数,那么从这个位置往后,从左到右都必须有默认值
  • 如果函数声明有默认参数,函数实现就不能有默认参数 (会出现二义性,声明和实现只能有一个默认参数)

函数声明: int func (int a ,int b);

4.2 函数占位参数

C++中函数的形参列表可以有占位参数,用来做占位,调用函数时必须填补该位置

语法: 返回值类型 函数名 (数据类型){ }

占位参数还可以有默认参数。 如: void fun(int a, int b =10) { }

4.3 函数重载

作用:函数名可以相同,提高复用性

函数重载满足条件: 

  • 同一个作用域下(如都在全局作用下)
  • 函数名称相同
  • 函数参数类型不同,或个数不同,或顺序不同

注意:函数的返回值不可以作为函数重载的条件 int func(int a){};double func(int a) {} //错误

当函数重载碰到默认参数,会出现二义性,报错,尽量避免这种错误

当引用作为函数重载时, int & a  和 const int & a 也发生了函数重载 

5. 类和对象-封装

C++面向对象三大特性:封装,继承,多态

万事万物皆为对象,对象上有其属性和行为

具有相同性质的对象,我们可以抽象为类,比如说人属于人类

封装的意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

语法:class 类名 {访问权限: 属性/行为  }

类中的属性和行为,我们统一称为成员

属性又叫 成员属性或成员变量                     行为又叫 成员函数或成员方法

在类中,可以让另一个类作为本来中的成员

class Student

{

        //权限

        public:

            // 属性

            string name;

             int ID;

             //行为

            void show(){ cout<<"hello"<<endl;}

};

创建一个具体学生,实例化对象:Student S1;

访问权限有三种:

  •  piblic  公共权限;   类内类外都可以访问
  • protected  保护权限;  类内可以访问,类外不可以访问(子类可访问父类保护的内容)
  • private  私有权限   类内可以访问,类外不可以访问(子类不可访问)

在C++中,struct和class唯一的区别在于 默认的访问权限不同,struct默认权限为公共,class默认权限为私有

成员属性设置为私有的优点:

  • 将所有成员属性设置为私有,可以自己控制读写权限
  • 对于写权限,我们可以检测数据的有效性

6. 类和对象-对象特性

6.1 构造函数和析构函数

构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用

析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作

构造函数和析构函数会被编译器自动调用,完成对象初始化和清理工作

如果我们不提供构造和析构,编译器会提供,编译器提供的构造函数和析构函数是空实现

构造函数语法: 类名( ) { }

  • 构造函数,没有返回值也不写void
  • 函数名称与类名相同
  • 构造函数可以有参数,因此可以发生重载
  • 程序在调用对象时会自动调用构造,无需手动调用,而且只会调用一次

析构函数语法: ~类名( ) { }

  • 析构函数,没有返回值也不写void
  • 函数名称与类名相同,在名称前加~
  • 析构函数不可以有参数,因此不可以发生重载
  • 程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次

6.2 构造函数被的分类及调用

按参数分为:有参构造和无参构造(默认构造)

按类型分为:普通构造和拷贝构造 ( 语法:类名(const 类名 &对象){ }  一样的数据类型,加const修饰,引用的方式)

三种调用方式:括号法( Person p1(p2) ),显示法( Person p1 = Person(p2) ),隐式转换法( Person p4 = 10)

注意事项:

  • 调用默认构造函数时,不要加(),因为编译器会认为是一个函数的声明,不会认为在创建对象,如Person p(); 
  • Person(10),这种类型被叫做匿名对象,特点是当前行执行结束后,系统会立即回收掉匿名对象
  • 不要利用拷贝构造函数,初始化匿名对象,如Person (p2); 这样 编译器会认为Person (p2) == Person p2,这是一个对象声明,会显示重定义的报错

6.3 构造函数调用规则

默认情况下,C++编译器至少给一个类添加3个函数:

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不提供默认无参构造,但会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数

6.4 深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作 (存在问题: int *height;    编译器会在默认拷贝构造中实现 height = p1.height,会出现堆区内存重复释放的问题)

深拷贝:再堆区重新申请空间,进行拷贝操作

浅拷贝带来的问题是 堆区的内存重复释放——解决方法:深拷贝(自己实现拷贝构造函数  height = new int (*p1.height))

析构代码,可以将堆区开辟的数据做释放操作,具体为:假设在类中定义 int *height

则在最后释放时,需要在析构函数中添加代码:

if(height ! = NULL){

        delete height;

        height = NULL;

}// 仍需要自己实现拷贝构造函数,解决浅拷贝带来的问题

如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题

6.5 初始化列表

作用:用来初始化属性

语法: 构造函数():属性1(值1),属性2(值2)... { }

6.6 类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员

如: class A { };       class B { A  a },则B类中有对象A作为成员,A为对象成员

当其他类对象作为本类成员,构造时候先构造类对象,再构造自身;析构时先析构自身,再析构类对象(析构的顺序与构造相反)

6.7 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为静态成员变量和静态成员函数

静态成员变量:

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化

静态成员变量不属于某个对象上,所有对象共享同一份数据,因此静态成员变量有两种访问方式,即通过对象进行访问以及通过类名进行访问(作用域下 Person::变量

静态成员变量也是有访问权限的,私有静态成员变量访问不到

静态成员函数:

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量,而不可以访问非静态成员变量,因为无法区分到底是哪个对象的属性

静态成员函数也是有访问权限的,类外访问不到私有静态成员函数

6.8 this指针

6.8.1 成员变量和成员函数的存储

在C++中,类内的成员变量和成员函数分开存储

只有非静态成员变量才属于类的对象上(静态成员变量,非静态成员函数,静态成员函数均不属于类对象上)

class Person { }    Person p; 则sizeof(p)的大小为1字节,即空对象占用内存空间为1,C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置,每个空对象也应该有一个独一无二的内存地址

6.8.2 this指针

每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会供用一块代码,那么这一块代码是如何区分是哪个对象调用自己呢?——解决方案,this指针

this指针是一种特殊的对象指针,this指针指向被调用的成员函数所属的对象,this指针的本质是指针常量

this指针是隐含每一个非静态成员函数内的一种指针,this指针不需要定义,直接使用即可

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用return *this

C++中空指针也是可以调用成员函数的

6.8.3 const修饰成员函数

常函数:

  • 成员函数后加const,我们称这个函数为常函数     void showPerson() const { }
  • 常函数内不可修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改  mutable int B;

常对象:

  • 声明对象前加const称该对象为常对象   const Person p;
  • 常对象只能调用常函数,不能调用普通成员函数,因为普通成员函数可以修改属性

7. 类和对象-友元

友元的目的就是让一个函数或者类,访问另一个类中私有成员

友元的关键字为 friend    

友元的三种实现:全局函数做友元;类做友元;成员函数做友元

写法:

在函数声明,类,成员函数前加 friend,把 friend 函数声明,类,成员函数放在类中的第一行  

如 class Person{

  friend class Buli; //类

或者  friend  void Sett(int a); // 全局函数

或者  friend  void 类名::Sett(int a)  //成员函数

public:// 友元代码要放在权限的上面

   ......

}

8. 类和对象-C++运算符重载

运算符重载:对已有的运算符重新进行定义,赋予其另一种功能,已适应不同的数据类型

运算符重载,也可以发生函数重载

对于内置的数据类型的表达式的运算符是不可能改变的;不要滥用运算符重载

8.1 加号运算符重载

 函数名称由编译器起名,可以使用成员函数或全局函数重载+号

若定义一个类为 class Person {

public:

       Person operator+ (Person &p) {

                Person temp;
                temp.m_A =this->m_A+ p.m_A;
                temp.m_B = this->m_B + m_B;
                return temp;

      }

    ...........

}

后续可以使用Person p3 = p1 + p2; //本质上为 Person p3 = p1.operator+(p2)

8.2 左移运算符重载

作用:可以输出自定义数据类型

当利用成员函数重载左移运算符  p.operator<< (cout)  简化版本为 p<<cout ,无法实现cout在左侧,所以我们不会利用成员函数去重载<<,只能利用全局函数去重载

如定义全局函数:

ostream  &operator<< (ostream &cout, Person &p){

                               //本质 operator<<(cout , p) 简化为cout<< p,cout属于输出流对象

        cout<< p.m_A;

        cout<<p.m_B;

        return cout;

}// 链式编程思想

后续可以使用 Person p;   cout<<p<<endl;

8.3 递增运算符重载

作用:通过重载递增运算符,实现自己的整型数据

前置递增返回引用,后置递增返回值

在成员函数中写入

class MyInteger {
    friend ostream& operator<<(ostream& cout, MyInteger myint); //对于左移运算符的重载  后面不是引用传递
public:
    MyInteger() {
        m_Num = 0;
    }

    //重载++运算符 前置++
    MyInteger &operator++() {   
//返回引用是为了一直对一个数据进行递增操作
        m_Num++;  //先++
        return *this;   //在将自身做返回
    }

    //重载++运算符 后置++
    MyInteger  operator++(int) { 
//区分前置和后置递增,在括号中写入int,表示占位参数  返回值
        MyInteger temp = *this;   //先记录当时结果
        m_Num++;   // 后递增

          return temp;   // 最后将记录结果做返回
    }

private:
    int m_Num;
};

后续可以使用 MyInteger myint;  cout<< ++myint <<endl;

8.4 赋值运算符重载

C++编译器至少给一个类添加4个函数

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性进行值拷贝
  • 赋值运算符operator=,对属性进行值拷贝 

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝完问题

在类Person中:

    Person &operator=(Person &p) {
        //应该先判断是否有属性在堆区,如果有先释放干净,然后在深拷贝
        if (m_Age != NULL) {
            delete m_Age;
            m_Age = NULL;
        }
        //深拷贝
        m_Age=new int(*p.m_Age);
        //返回对象本身
        return *this;
    }

    int* m_Age;

后续可以使用p3 = p2 = p1;   // Person p1;.....

8.5 关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

在定义的类中:

bool operatop==(Person &p){

     if(this->m_Name==p.m_Name && this->m_age==p.m_age) { return true; }

      return false;

}

后续可以使用 Person p1;  Person p2;     if(p1 == p2) { }

8.6 函数调用运算符重载

函数调用运算符()也可以重载,称为仿函数

仿函数没有固定写法,非常灵活

void operator() (string test) { cout << test<<endl; }

9. 类和对象-继承

继承的好处:减少重复代码

语法:  class 子类 : 继承方式  父类

子类也成为派生类,父类也成为基类

派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员

继承方式一共有三种:公共继承,保护继承,私有继承

若子类以公共继承方式继承父类,则

  • 父类中的公共权限成员,到子类中依然是公共权限
  • 父类中的保护权限成员,到子类中依然是保护权限
  • 父类中的私有权限成员,子类访问不到
  • 类外可以访问公共权限成员,其他访问不到

若子类以保护继承方式继承父类,则

  • 父类中的公共权限成员与保护权限成员,到子类中是保护权限
  • 父类中的私有权限成员,子类访问不到
  • 类外不可以访问权限成员

若子类以私有继承方式继承父类,则

  • 父类中的公共权限成员与保护权限成员,到子类中是私有权限
  • 父类中的私有权限成员,子类访问不到
  • 类外不可以访问权限成员

父类中所有非静态成员属性都会被子类继承下去,父类中的私有成员属性,是被编译器隐藏了,因此访问不到,但确实被继承下去了。

如父类中定义三种权限成员各一个,数据类型是int,子类中以公共方式继承并定义一个数据类型为int 的成员,则sizeof(子类)的大小为16字节

继承中的构造和析构顺序如下:先构造父类,在构造子类,析构的顺序与构造的顺序相反(即先析构子类,在析构父类)

9.1 继承同名成员处理

当子类和父类出现同名的成员(包括静态成员),如何通过子类对象,访问到子类或父类中同名的数据:

  • 访问子类同名成员,直接访问即可  s1.m_A
  • 访问父类同名成员,需要加作用域  s1.Base::m_A

class Base {

public:
    Base() {
        m_A = 100;
    }

    int m_A;
};

class Son : public Base {
public:
    Son() {
        m_A = 200;
    }
    int m_A;
};

    cout << s1.m_A << endl; 
    cout << s1.Base::m_A << endl;

如果子类中出现和父类同名的成员函数(包括静态成员函数),子类的同名成员会隐藏掉父类中所有同名成员函数,如果想访问父类中被隐藏的同名成员函数,需要加作用域

同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类名)

9.2 多继承语法

C++允许一个类继承多个类

语法: class 子类 :继承方式  父类1,继承方式 父类2

多继承可能会引发父类中有同名成员出现,需要加作用域区分

实际开发中不建议用多继承

9.3 菱形继承

两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承被被称为菱形继承,或者钻石继承 

当菱形继承,两个父类拥有相同数据,需要加以作用域区分;这份数据其实只有一份就可以,菱形继承导致数据有两份,造成资源浪费

利用虚继承,解决菱形继承的问题,即在权限之前,加上关键字 virtual 变为虚继承

如 class Animal {};   

class sheep : virtual public Animal {};

10. 类和对象-多态

多态分为两类:

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态的区别:

  • 静态多态的函数地址早绑定-编译阶段确定函数地址
  • 动态多态的函数地址晚绑定-运行阶段确定函数地址

可通过虚函数实现对地址的晚绑定,如父类中的成员函数和子类中的相同,则需要在成员函数前加virtual变成虚函数

如:class  Animal {

public:

        virtual void speak() { .... }   //  虚函数

}

动态多态满足的条件:

  • 有继承关系
  • 子类重写父类的虚函数

函数重写概念: 函数返回值类型,函数名,参数列表完全相同

动态多态的使用:父类的指针或者引用,指向子类对象

如:void  DoSpeak (Animal &animal ) {

       animal.speak();

        delete animal;

}

在测试案例中,有一个类Cat,定义Cat  cat;   DoSpeak(cat); // 本质是Animal &animal = cat;   

或者   void  DoSpeak (Animal * animal ) {

       animal->speak();

}

在测试案例中,有一个类Cat,定义DoSpeak(new Cat);

10.1 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数

纯虚函数的语法:virtual 返回值类型 函数名 (参数列表)= 0;

当类中有了纯虚函数,这个类也成为抽象类

抽象类的特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

如:class Base {
public:
    virtual void func() = 0;  //纯虚函数
};

class Son:public Base {
public:
    void func() {} // 重写纯虚函数
};

10.2 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,(也可以说为 父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,会出现内存泄露),解决方式是将父类中的析构函数改为虚析构或者纯虚析构,以此解决父类指针释放子类对象时不干净的问题

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:如果是纯虚析构,该类属于抽象类,无法实例化对象

纯虚析构,既需要声明也需要实现

虚析构语法:virtual ~类名 ( ) { }

纯虚析构语法:virtual ~类名 ( ) = 0;  类名 :: ~类名 ( ) { }

如果子类中没有堆区数据(即new 数据),可以不写为虚析构或纯虚析构

11. 文本操作

程序运行时产生的数据属于临时数据,程序一旦运行结束都会被释放,通过文件可以将数据持久化

C++中对文件操作需要包含头文件 <fstream>

文件类型分为两种:

  • 文本文件:文件以文本的ASCII码形式存储在计算机中
  • 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们

操作文件的三大类:

  • ofstream:写操作
  • ifstream:读操作
  • fstream:读写操作

 11.1 写文件步骤-文本文件

1. 包含头文件:#include <fstream>

2. 创建流对象 :ofstream ofs;

3. 打开文件:ofs.open("文件路径",打开方式);

4. 写数据:ofs<<"写入的数据"<<endl; //可以换行

5. 关闭文件:ofs.close();

打开方式:

  • ios::in   为读文件而打开文件
  • ios::out  为写文件而打开文件
  • ios::ate  初始位置:文件尾
  • ios::app  追加方式写文件
  • ios::trunc  如果文件存在先删除,在创建
  • ios::binary  二进制方式

文件打开方式可以配合使用,利用 | 操作符

如:用二进制方式写文件  ios::binary | ios::out

11.2 读文件步骤-文本文件

1. 包含头文件:#include <fstream>

2. 创建流对象 :ifstream ifs;

3. 打开文件并判断文件是否成功打开:ifs.open("文件路径",打开方式);    if(! ifs.is_open()) {cout<<"文件打开失败"<<endl;  ;return; }

4. 读数据:四种读取方式

5. 关闭文件:ifs.close();

四种读取数据方式:

第一种:

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; }

11.3 写文件步骤-二进制文件

1. 包含头文件:#include <fstream>

2. 创建流对象 :ofstream ofs;

3. 打开文件:ofs.open("文件路径",ios::out | ios::binary);

4. 写数据:ofs.write();  这里假设有一个类class Person { };   Person p ={ "张三",18 };  ofs.write ( (const char *)&p,sizeof(Person) );

5. 关闭文件:ofs.close();

以二进制的方式对文件进行读写操作,打开方式要指定为 ios::binary

二进制方式写文件主要利用流对象调用成员函数 write

函数原型:ostream& write ( const char * buffer, int len );

参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数

11.4 读文件步骤-二进制文件

1. 包含头文件:#include <fstream>

2. 创建流对象 :ifstream ifs;

3. 打开文件并判断文件是否成功打开:ifs.open("文件路径",ios::in | ios::binary);    if(! ifs.is_open()) {cout<<"文件打开失败"<<endl;  ;return; }

4. 读数据   ifs.read();   这里假设有一个类class Person { };   Person p;  ifs.read ( (char *)&p,sizeof(Person) );   cout<<...<endl;

5. 关闭文件:ifs.close();

二进制方式读文件主要利用流对象调用成员函数 read

函数原型:istream& read ( char * buffer, int len );

参数解释:字符指针buffer指向内存中一段存储空间,len是读写的字节数

;