C++笔记(三)类和对象1

C++笔记(三)类和对象1

封装
  • 就是class类,类中需要包含成员属性(成员变量)和成员行为(成员函数),还有访问权限:public、private、protected。如果不写访问权限,默认就是private权限
  • 访问权限:
    • public:类内和类外都可以访问
    • private:类内可以访问,类外不可以访问
    • protected:类内可以访问,类外不可以访问(在后续的继承讲解)
  • class类与struct结构体的区别:class类可以选择访问权限,且默认是私有访问权限;struct结构体默认是公共访问权限
  • 访问私有成员的方法:可以在公共成员函数写私有成员变量的读和写函数
  • 一般习惯把成员变量放在私有访问权限中,然后根据成员变量是否可读还是可写,写出对应的可读(get)和可写(set)函数
class a{                //建立一个a的类
  public:         //访问权限公共
    int val;       //公共成员变量
    void func1();       //公共成员函数声明,在类外进行定义函数内容
  private:         //访问权限私有
    void func2(){     //私有成员函数
    	...        //成员函数可以直接在类内进行定义内容
    }
};             //创建类的对象,与结构体一模一样
void a::func1(){        //定义类内的成员函数,需要使用::运算符
    ...
}
对象的初始化和清理
构造函数
  • 语法:类名(){}
  • 构造函数,没有返回值也不写void
  • 函数名称与类名相同
  • 构造函数可以有参数,因此可以发生重载
  • 程序在创建对象的时候会自动调用构造函数,且只会调用一次
  • 如果没有写构造函数,编译器会自动生成一个空的构造函数
析构函数
  • 语法:~类名(){}
  • 析构函数,没有返回值也不写void
  • 函数名称与类名相同,在名称前添加~
  • 析构函数不可以有参数,因此不会发生重载
  • 程序在对象销毁前会自动调用析构函数,且只会调用一次
  • 如果没有写析构函数,编译器会自动生成一个空的析构函数
class student{
  public:
    student(int a){}         //构造函数
    ~student(){}        //析构函数
};
int main(){
    student s1; //创建对象的时候,会自动调用一次构造函数
    return 0;   //返回之后,程序的内存被释放,在此之前会运行一次析构函数
}
构造函数的分类
  • 分类:
    • 按照参数分类:有参构造和无参构造
    • 按照类型分类:普通构造和拷贝构造
  • 有参构造函数和无参构造函数根据是否有无参数分类,可以重载
  • 拷贝构造函数有固定的写法
  • 调用:
    • 括号法
    • 显示法
    • 隐式转换法
class Person{
  public:
    Person(){}                   //无参构造函数
    Person(int a){}         //有参构造函数
    Person(const Person &p){}   //拷贝构造函数:顾名思义,需要拷贝,那么需要将另一个类传入进来,并不能修改传入进来的类,所以需要使用const和引用的方法
    ~Person(){}                  //析构函数
  private:
    int age;
};
void test01(){                   //调用的方法如下
    //括号法
    Person p1;                   //默认构造函数调用
    Person p2(10);         //有参构造函数
    Person p3(p2);         //拷贝构造函数
    //注:拷贝构造函数会把被拷贝的类中成员复制一份给新的类
    //也就是说,此时p3的类与p2的类完全相同,包括成员变量的值都是一样的
    //注意事项:
    //默认构造函数不能使用(),因为编译器会认为这是一句函数的声明:
    //如:Person p1();这跟函数声明一样,编译器可以通过,但是不会创建类
    
    //显示法
    Person p1;
    Person p2 = Person(10);      //有参构造
    Person p3 = Person(p2);    //拷贝构造
    //注:Person(10);这是个匿名对象
    //匿名对象的特点:当前行执行结束,系统会立即回收匿名对象
    //就是说,匿名对象这行代码之后,系统直接回收,运行了析构函数
    //注:不要使用拷贝构造函数,初始化匿名对象
    //如:Person(p3);这句话编译器会等同于Person p3;因此出现重定义
    
    //隐式转换法
    Person p4 = 10;     //等价于Person p4 = Person(10) 有参构造
    Person p5 = p4;   //拷贝构造
}
拷贝构造函数调用时机
  • 使用一个已经创建完毕的对象来初始化一个新的对象,就是传统的复制概念
  • 值传递的方式给函数参数传值,会调用拷贝构造函数
  • 值方式返回局部对象,会调用拷贝构造函数
class Person(
  public:
    Person(){}
    Person(int a){}
    Person(const Person &p){}       //拷贝构造函数
    ~Person(){}             //析构函数
);
//初始化新的对象:
void test1(){
    Person p1(20);
    Person p2(p1);
}
//值传递方式给函数参数传值
void dowork1(Person p){              //给函数参数传值
}                     //会在此时调用拷贝构造函数
  //相当于函数内部是使用了一个拷贝的副本运行,运行结束也会执行析构函数
void test2(){            
    Person p;              //定义一个类,调用了构造函数
    dowork1(p);             //使用函数参数传值
}
//返回值的方式调用拷贝构造函数
Person dowork2(){
	Person p1;                 //定义一个类,调用了构造函数
    return p1;          //返回一个类,此时会调用考别构造函数
}
  //相当于在函数返回时候,返回了一个拷贝的副本,运行结束会执行析构函数
void test3(){
    Person p = dowork2();    //运行完,就执行析构函数
}
构造函数调用规则
  • 默认写一个类的情况下,编译器至少会添加3个函数:
    • 默认构造函数(无参数,函数体为空)
    • 默认析构函数(无参数,函数体为空)
    • 默认拷贝构造函数,对属性值进行拷贝
  • 规则如下:
    • 如果用户定义有参构造函数,编译器就不会提供默认构造函数,但是会提供默认的拷贝构造函数
      • 就是说,你写了有参构造函数,而你调用默认构造函数时,编译器会找不到,但是拷贝构造函数还是有的
    • 如果用户定义拷贝构造函数,编译器不会提供其他构造函数
      • 跟上面一样,如果写了拷贝构造函数,而你调用了其他构造函数,编译器就找不到了
深拷贝和浅拷贝(面试经典)
  • 浅拷贝:简单的复制拷贝操作(默认的拷贝构造函数)
  • 深拷贝:在堆区重新申请空间,进行拷贝操作
  • 浅拷贝带来的问题:重复的释放同一段堆区内存,使得出现非法操作
//浅拷贝问题所在:
class Person(){
  public:
    Person(){}
    Person(int a, int height){
        m_age = a;
        m_height = new int(height);  //把数据放在堆区
    }
    ~Person(){
        //自己管理的堆区数据,一般都在析构函数对其进行释放
        if(m_height != NULL){
            delete m_height;
            m_height = NULL;
        }
        cout << "析构函数" << endl;
    }
    
    int m_age;
    int *m_height; //用指针是因为我们要把数据放在堆区,自己管理
             //那么我们就需要有对其进行释放地方,析构的用处在此
};
void test1(){
    Person p1(10, 160);
    Person p2(p1);   //由于我们没有写拷贝构造函数,因此只是简单的
                //赋值操作,是浅拷贝
    cout << *p2.m_height << endl; //因为定义的是个指针,所以解引用
}
  • 上面的代码就会出现浅拷贝的问题,程序会出错:非法操作。
    • 首先是堆区的数据是先入后出的,所以释放时也是先释放的p2中的(其实可以不管这个顺序的,因为指向的是同一个堆区数据)
    • 然后我们没有写拷贝构造函数,因此p2的拷贝是简单的赋值拷贝(浅拷贝),而m_height参数是一个指针(地址),仅仅将地址赋值给p2了,只不过这个地址指向的是一个堆区
    • 最后就是问题所在,p2被释放的时候,析构函数将m_height指向的堆区数据释放了,然而在p1被释放的时候,析构函数又一次将m_height指向的堆区数据释放,出现了释放一块未指定的区域,造成非法操作。下面用图展示:

image-20220313102234078

  • 下面就使用深拷贝的方式解决这个问题,就是让p2指向另一块堆区数据,数据值相同,但是这样两次释放数据就不会重复释放同一块了。用图展示:

image-20220313102401313

//代码结构都如上面的代码结构
//我们不使用编译器自己默认添加的拷贝构造函数,我们自己写拷贝构造函数,然后每一次都在堆区新建一个数据,就不会出现重复释放的问题:
//上面的代码添加这段我们自己写的拷贝构造函数
Person(const Person &p){ 
    m_age = p.m_age;
    //m_height = p.m_height    //编译器默认添加的就是这个
    m_height = new int(*p.m_height);
    //我们自己添加new,因为m_height是一个指针,所以要使用解引用
}
初始化列表
  • 语法:构造函数():属性1(值1),属性2(值2) ... {}
  • 可以有更加灵活的方式
class Person{
  public:
    //传统的初始化方式:
    //Person(){
    //    m_a = a;
    //    m_b = b;
    //}
    //初始化列表的方式:
    Person():m_a(10),m_b(20){   //直接初始化
        //正常的函数体
    }
    //更加灵活的初始化列表:
    Person(int a, int b):m_a(a),m_b(b){
        //正常的函数体,跟上面的初始化列表,二选一
    }
    int m_a;
    int m_b;
};
void test1(){
    Person p;      //使用初始化列表的方式,自动初始化类中变量
    Person p(10,20);  //使用更加灵活的初始化列表方式
               //可以自定义初始化的值
}
类对象作为类成员
  • 就是在类中嵌套了另一个类,如,B类中有一个A类
    • 构造函数的执行顺序:先执行类中成员的构造函数,再执行外面类的构造函数。就是说,先执行A的,再执行B的
    • 析构函数的执行顺序:先执行类外的析构函数,在执行类中成员的析构函数。就是说,先执行B的,在执行A的
class Phone(){
  public:
    string m_pname;
};
class Person(){
  public:
    Person(string name, string p): m_name(name),m_phone(p){
        ...
    }
    string m_name;
    Phone m_phone;         //类中嵌套了一个类
};
void test(){
    Person("asdsa", "sdasdd");  
    //先执行的Phone的构造函数,再执行的Person的构造函数
    //先执行的Person的析构函数,在执行的Phone的析构函数
}
静态成员
  • 静态成员变量
    • 特点:
      • 所有对象共享一份数据,就是说,不是某一个对象的数据,而是所有对象共享的,任意对象修改,都会更改其他对象,就是一块内存。
      • 在编译阶段分配内存
      • 类内声明,类外初始化,使用类的::域运算符在类外对静态成员变量初始化,而如果是私有访问权限的,不能这么初始化,可以使用成员函数进行初始化赋值
    • 静态成员变量的访问方式
      • 通过对象的方式访问,就是正常的成员变量的访问方式
      • 通过::运算符访问
class Person{
  public:
    static int m_a;          //类内定义静态成员变量
};
int Person::m_a = 10;     //静态成员变量初始化
void test1(){
    Person p1;
    p1.m_a = 100;             //可以使用对象的方式访问
    Person p2;
    p2.m_a = 200;   //因为共享一块数据,所以p2的修改也影响p1的值
    cout << Person::m_a << endl; //可以使用::的方式访问
}
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量
class Person(){
  public:
    static void func(){    //静态成员韩式
        m_a = 100;      //只能访问静态成员变量
        //m_b = 200;       //不能访问普通成员变量
    }
    static int m_a;
    int m_b;
};
int Person::m_b = 10;    //静态成员变量的初始化
void test(){
    Person p1;
    p1.func();              //普通的对象访问方式
    Person::func();      //使用::运算符访问
}
C++对象模型和this指针
成员变量和成员函数分开存储
  • 类的成员变量和成员函数分开存储
  • 只有非静态成员变量才属于类的对象上,就是说只有普通的成员变量才在类的对象上
class Person{};        //空对象,使用sizeof之后,会发现占用了1字节
              //是编译器为空对象所占用的
class Person{
    int m_a;        //普通的成员变量,sizeof之后为4
};                      //只有普通的成员变量才属于类的对象
class Person{
    int m_a;       //sizeof之后还是为4
    static int m_b;   //静态成员变量,不属于类的对象
};
class Person{
    int m_a;
    static int m_b;   //sizeof之后还是为4
    void func();        //成员函数,不属于类的对象
};  //因为函数只有一份,各个对象都可能调用,不会在某一个单独的对象中
class Person{
    int m_a;
    static int m_b;   //sizeof之后还是为4
    void func();     //静态成员函数,不属于类的对象
    static void func1();
};
this指针的用途
  • this指针指向被调用成员函数所属的对象
    • this指针是隐含每一个非静态成员函数内的一种指针
    • this指针不要定义,直接使用就行
  • 用途:
    • 当成员函数的形参与成员变量同名时,可用this指针用来区分
    • 在类的非静态成员函数中返回对象本身,可使用return *this
class Person{
    //问题1:解决同名冲突
    Person(int age){   //非静态成员函数的形参与成员变量同名
        //age = age;  //这时要赋值的时候,编译器就找不到这个值是那个
        this->age = age;   //可以使用this指针,指向p1的对象
                           //表示这个age是对象中的成员变量的age
    }
    //问题2:返回对象本身
    void Personaddage(Person &p){
        this->age += p.age;   //如果仅仅这么写,无法完成链式相加
    }
    //可以使用返回this,实现链式编程
    Person& Personaddage(Person &p){
        this->age += p.age;
        return *this      //指的就是返回对象本身
                     //this指向对象,然后解引用,就是对象本身
    }
    int age;
};
void test(){
    Person p1(10);
    Person p2(10);
    p2.Personaddage(p1);    //对于问题2,我们想把两个对象的age相加
    //对于问题2,我们不止想加一次,怎么才能这样写:
    p2.Personaddage(p1).Personaddage(p1).Personaddage(p1);
    //这种是链式编程思想,下面用图表示一下
}

image-20220313204931278

空指针访问成员函数
  • 空指针可以调用成员函数,但是需要注意在函数中是否使用this指针,实际上成员变量在函数中,编译器都会在前面添加一个this->
  • 如果有使用this指针,需要在函数调用开头,进行判断
class Person{
    void showPerson1(){           //成员函数没有使用成员变量
        cout << "asdad" << endl;
    }
    void showPerson2(){       //成员函数使用了成员变量
        cout << m_age << endl;   //编译器会在age前自动添加this->
    }
    void showPerson3(){
        if(this == NULL){          //如果使用空指针指向对象
            return;          //就要进行判断
        }
    }
    int m_age;
};
void test(){
    Person *p = NULL;        //创建一个空指针,指向对象
    p->showPerson1();     //调用成员函数,没有使用成员变量
                   //不会报错
    //p->showPerson2();    //调用成员函数,使用了成员变量
    //会报错,因为这本来是个空指针,没有对象中的成员变量,
     //这时候空指针找不到那个不存在的成员变量
    p->showPerson3();        //所以,需要使用这个判断
}
const修饰成员函数
  • 常函数:
    • 成员函数后面加const,就是常函数
    • 常函数内不可以修改成员变量
    • 如果有特殊的成员变量需要常函数修改,需要在成员变量前加mutable(强制转换const)
  • 常对象
    • 声明对象前加const,就是常对象
    • 常对象只能调用常函数(因为如果能够调用普通成员函数,而普通成员函数能够修改成员变量,那么const常对象就无意义了)
    • 常对象依然可以修改mutable的成员变量
class Person{
  public:
    void shuwPerson() const{   //函数后面添加一个cosnt,就是常函数
        //m_a = 10;            //常函数中不能修改普通的成员变量
        m_b = 20;              //但是可以修改mutable的成员变量
    }
    int m_a;
    mutable int m_b;
};
void test(){
    const Person p;            //声明对象之前加const,就是常对象
    //p.m_a = 100;        //常对象不能修改成员变量
    p.m_b = 200;               //但是可以修改mutable的成员变量
}
友元
  • 友元函数可以访问类中的私有成员
全局函数做友元
class Person{
    friend void func(Person *p);   //全局函数做友元
  public:
    int m_a;
  private:
    int m_b;              //正常全局函数无法访问私有成员
};
void func(Person *p){         //全局函数声明放在类中做友元
    p->m_a = 10;            
    p->m_b = 20;            //因此可以访问类中私有成员
}
类做友元
  • 类做友元,可以访问其他类中的私有成员
class Phone{};
class Person{                    //定义两个类
    friend class Phone;      //使用友元
    //那么Phone类中的函数可以访问Person类中的私有成员
};
成员函数做友元
  • 类中成员函数做友元,访问另一个类中的私有成员
class Phone{
  public:
    void show();
};
class Person{
    friend void Phone::show();    
    //如果没有Phone,编译器就会认为这是全局友元函数
    //这个show函数就可以访问Person类中的私有成员
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值