程序员

一、类与对象

2020-05-08  本文已影响0人  __bba3

1. 认识类与对象

(1)什么是类(class)?

类(class)是类型(type),是用户自定义的类型。为什么不叫它type,因为借用Simula语言中的class关键字。

<1>为什么要有类?

基于便利性的考虑,现实世界中物(object)通常被分为几种独立的分类。

<2>基本概念
<3>面向对象的四大特征
<4>面向对象的五大原则

1.单一职责原则
2.开放封闭原则
3.替换原则
4.依赖倒置原则
5.接口隔离原则

2.类的定义与对象创建

(1)类的定义:与struct相似(C++)

<1>格式
class 类名{
       成员变量/成员函数声明;
};

注意:class定义最后的;一定不要忘记。

<2>构成
//成员函数可以自己调用内部其他的成员函数
void Print(){
        cout << name << '\t' << count << "\t¥"  << price << "\t¥"  << GetTotal() << "\t¥" << GetOff() << endl;
}
<3>作用域运算符:: -- 函数归属

在类中定义,在类外面进行实现。如果在类里面实现的话,不需要该格式

在类外实现,使用该格式:
返回类型  类名::函数名
如:
void Student::Print(){}
<4>访问限定符

private[默认]:私有
public:公有
protected:保护
实践中,成员变量多数情况使用private或者protected,成员函数多数情况使用public。通常,通过成员函数改变对象的成员变量。

<5>类定义与类实现分离(.h和.cpp)

声明和实现分开编写。

<6>class与struct的区别

C++的class与struct的区别

struct在C和C++中的区别
1.C++的struct可以添加成员函数,而C不可以
2.C++的struct可以使用访问控制关键字(public private protected),而C不可以。
3.C++的struct在定义对象时可以忽略,而在C中不可以。

SPos spos; // C++
struct SPos spos; // C/C++

(2)对象创建/实例化

<1>直接创建 --类作为类型定义变量 --栈上创建
//基本类型
int  a=10;
int b(10);//等价于int b=10;
//类类型
class Demo{};
//创建变量格式:类名 对象名;  // 创建对象,自动调用默认构造函数
Demo d;
Student zhangsan("张三",true,21);//创建对象时可以直接赋值
//创建匿名对象格式:类名(); // 创建匿名对象
Demo();
<2>动态创建 --堆上创建
//基本类型
int* p = new int;
delete p;
p = NULL;
//对象指针new可以为对象设置初始值
int* p = new int(100);
cout << *p << endl;
//类类型
class Demo{};
//类名* 对象指针 = new 类名;// 调用默认构造函数
Demo* d=new Demo;
Student* lisi =new Student("李四",false,22);//可以为对象设置初始值
delete 对象指针;
<3>动态创建数组 -- 堆上创建
//基本类型
int* pa = new int[10];
delete pa;// 只释放p[0]
delete [] pa;// 释放全部数组
//类类型
Demo* d = new Demo[10];
delete [] d;
d = NULL;
//注意:对象数组指针new不可以为对象设置初始值。
int* pa = new int[10](100);//错误:parenthesized initializer

注意:
1.空结构体与空类的大小(sizeof)为1,主要在于初始化/实例化时,编译器给变量/对象分配内存(地址),内存最小单位为1个字节。通常,sizeof(类型) == sizeof(变量)。


为什么空的class需要分配一个字节?
类的实例化就是在内存中分配一块地址,空类同样可以被实例化,每个实例在内存中都有一个独一无二的地址,因此需要一块内存来存放该地址,而c++中可以申请的最小单位就是一个字节。


3.方法

(1)构造函数

<1>语法
类名(参数){
  函数体
}
<2>特点

1.构造函数的函数名与类名相同
2.在对象被创建时自动执行
3.没有返回值类型、也没有返回值
4.可以有多个构造函数(构造函数的重载),但是在调用时,会根据需要调用其中一个构造函数

<3>构造函数

1.默认构造函数:没有参数的写法。当没有编写构造函数时,如果创建对象,系统会自动调用默认的构造函数,但是该构造函数的函数体空的,不能够达到初始化的目的,编译可能不会出错,执行时会吐核。但是如果定义了构造函数,不管是否合适,编译器都不会调用默认的构造函数。
2.构造函数的重载:使用类名相同的构造函数,并且是有参数的,有赋值功能。主要用在从外边传入已经分配好的动态内存,提供一个接口。

<4>调用时机

1.使用类直接创建对象
2.new动态创建

<5>构造函数的作用

1.给创建的对象建立一个标识符
2.为对象数据成员开辟内存空间
3.完成对象数据成员的初始化
例子:

class Array{
private:
      int* data;
      int length;
public:
      Array(){//构造函数
            data=NULL;
             length=0;
      }
      Array(int* d,int l){//构造函数的重载
            data=d;
            length=l;
      }
};

int main{
      //调用默认的构造函数
      Array temp;
      //调用构造函数的重载:
      int* list=new int[10];//创建了动态分配内存
      for(int i=0;i<10;++i){
            list[i]=i;
       }
      Array(list,10);//调用上面的构造函数重载,从外边传入已经分配好的动态内存。
}

(2)初始化列表

初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
主要是性能问题,使用初始化列表少了一次调用拷贝构造函数的过程,这对于数据密集型的类来说,是非常高效的。

<1>语法
类名(参数):成员变量1(参数1),成员变量2(参数2){
      //函数体
}
<2>作用

初始化非静态成员变量

<3>说明

1.必须使用初始化列表的情况:

2.初始化列表与构造函数内部成员赋值的区别: 成员变量初始化与成员变量赋值

注意:能使用初始化列表的时候尽量使用初始化列表

例子:(一般都是写成初始化列表的形式)
把上面的构造函数写成初始化列表的方式:

Array():data(NULL),length(0){}
Array(int* d,int l):data(d),length(l){}
<4>成员变量的初始化顺序
class Demo1{};
class Demo2{};
class Demo3{};
class Test{
public:
        Test():d1(),d3(),d2(){}
private:
      Demo1 d1;
      Demo2 d2;
      Demo3 d3;
int main(){
      Test test;
//初始化顺序结果为:d1,d2,d3。
}

成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。一般情况定义成员变量的顺序和初始化成员列表顺序一致。

<5>代码(账单bill)
#include <iostream>
#include <vector>
using namespace std;
class Record{
private:
    string name;
    int count;
    float price;
    float off;
public:
    Record(string name,int count,float price):name(name),count(count),price(price),off(1){}
    Record(string name,int count,float price,float off):name(name),count(count),price(price),off(off){}
    float GetTotal(){
        return count*price*off;
    }
    float GetOff(){
        return count*price*(1-off);
    }
    void Print(){
        cout << name << '\t' << count << "\t¥" << price << "\t¥" << GetTotal() << "\t¥" << GetOff() << endl;
    }
};
class Bill{
private:
    vector<Record> records;
public:
    void Add(Record r){
        records.push_back(r);
    }
    void Print(){
        cout << "物品\t数量\t单价\t总价\t节省\n";
        cout << "-------------------------------------" << endl;
        float totalPrice = 0;
        float totalOff = 0;
        for(int i=0;i<records.size();++i){
            records[i].Print();
            totalPrice+= records[i].GetTotal();
            totalOff+=records[i].GetOff();
        }
        cout << "-------------------------------------" << endl;
        cout << "总价:¥" << totalPrice << '\t' << "总节省:¥" << totalOff << endl;
    }

};

int main(){
    /*
    Record r("苹果",3,3.5);
    Record r2("桔子",4,5.5);
    Record r3("梨",2,1.5);
    Record("香蕉",4,4.5);
    */
    Bill b;
    b.Add(Record("苹果",3,3.5));
    b.Add(Record("桔子",4,5.5));
    b.Add(Record("梨",2,1.5,0.7));
    b.Add(Record("香蕉",4,4.5,0.8));
    b.Print();
}

(3)析构函数

构造函数是必须的,但是析构函数有时不需要!!,当没有申请新的空间时,没有必要编写析构函数,来释放空间,系统会自动调用系统的析构函数。

<1>语法
~类名(){
    函数体
}
<2>特点

1.析构函数的函数名与类名相同
2.函数名前必须有一个~
3.没有返回值类型、也没有返回值
4.只能有一个析构函数
5.没有参数

<3>调用时机

1.对象离开作用域(作用域是以{ }开始和结束的)
2.delete调用delete时执行,与作用域无关。

<4>默认析构函数

类中没有显式定义的析构函数,编译器就会自动为该类型生成默认析构函数

<5>作用

释放对象所申请占有的资源

<6>析构顺序

析构顺序和声明顺序完全相反,最先构造的最后析构。

C++RAII机制:
RAII(资源的取得就是初始化,Resource Acquisition Is Initialization)。步骤:1 申请资源;2 使用资源;3 释放资源。C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。 -- 百度百科


new/delete与malloc()与free()的区别?
1.new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持<stdlib.h>
2.new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void指针转换成我们需要的类型。
3.
new和delete会自动调用构造函数和析构函数;但是malloc和free不会自动调用构造函数和析构函数。
4.使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
5.new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。


(4)this指针

this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。所谓当前对象,是指正在使用的对象。例如对于stu.show(),stu 就是当前对象,this 就指向 stu。

<1>作用域

类的内部。

<2>特点

如果成员函数形参与成员变量同名,使用this->做为前缀区分。

class Student{
public:
    void setname(string name);
    void setage(int age);
    void show();
    
private:
    string name;
    int age;
};
void Student::setname(string name){
    this->name = name;//创建的类中成员变量也有name,使用name=name是无效的
//它的形参是name,和成员变量name重名,如果写作name = name;
//这样的语句,就是给形参name赋值,而不是给成员变量name赋值
}
void Student::setage(int age){
    this->age = age;
}
void Student::show(){
    cout<<this->name<<"的年龄是"<<this->age<<endl;
}
void Student::printThis(){
    cout<<this<<endl;
}

int main(){
    Student *pstu = new Student;//创建了class指针
    pstu -> setname("jack");//必须通过->来访问
    pstu -> setage(16);
    pstu -> show();//jack的年龄是16

Student *pstu1 = new Student;
pstu1 -> printThis();
cout<<pstu1<<endl;
输出一样:0x7b17d8    0x7b17d8
Student *pstu2 = new Student;
pstu2 -> printThis();
cout<<pstu2<<endl;
输出一样:0x7b17f0     0x7b17f0
}
<3>实质:

this实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。


C++函数传参的三种方式:
访问内存的方式:变量,指针,引用
1.传值
//void Swap(int a,int b);
//Swap(n,m);
//等价于:int a=n;int b=m;(赋值操作,不能修改值)
2.传地址/指针
////void Swap(int* a,int* b);
//Swap(&n,&m);
//等价于:int* a=&n;int* b=&m;(把地址传进去,可以修改)
3.传引用
//void Swap(int& a,int& b);
//Swap(n,m);
//等价于:int& a=n;int& b=m;(引用,可以修改)


(5)引用(别名)

<1>语法
int a=10;
int& b=a;
cout << "&a:" <<&a <<'\t'<<"a:"<<a<<endl;
cout << "&b:" <<&b <<'\t'<<"b:"<<b<<endl;
a=100;
cout << "&a:" <<&a <<'\t<<'"a:"<<a<<endl;
cout << "&b:" <<&b <<'\t'<<"b:"<<b<<endl;
//执行结果:a和b的地址一样,值也一直一样,不管修改a的值或者b的值,都一样
&a:0x61ff08     a:10
&b:0x61ff08     b:10
&a:0x61ff08     a:100
&b:0x61ff08     b:100

引用其实就是一个别名,ab代表的是相同的对象。一个变量可以有多个别名。

    int a=10;
    int& c=a;
    int& d=a;
    cout << "&a:" << &a <<" "<< a <<endl;
    cout << "&c:" << &c <<" "<< c <<endl;
    cout << "&d:" << &d <<" "<< d <<endl;
/执行结果:
&a:0x61ff00 10
&c:0x61ff00 10
&d:0x61ff00 10
<2>何处使用引用?
//使用指针可以同样实现
void Swap(int& n,int& m){//作为参数
    //cout << "&n:" << &n << "\t&m:" << &m << endl;//同一块内存
    int t=m;
    m=n;
    n=t;
}
int main(){
    int n=10;
    int m=100;
    //cout << "&n:" << &n << "\t&m:" << &m << endl;//和上面是同样的地址
    Swap(n,m);
}
//当函数有多个返回值时,return可以返回一个,其他的可以使用引用来返回和指针类似
int divide(int n,int m,int& mod){//return返回div,mod以引用的方式返回
    int div=n/m;
    mod=n%m;
    return div;
}
    int mod;
    int div=divide(n,m,mod);
class Simple{
        int& n;
public:
  //引用成员变量必须在构造函数初始化列表中初始化
        Simple(int& n):n(n){}
        void Print(){
                cout << "&n:" << &n << '\t' << n << endl;  
        }
};
int main(){
      int m=10;
      Simple s(m);
      s.Print();
      m=100;
      s.Print();
}
//执行结果:
&n:0x61ff04     10
&n:0x61ff04     100
注意:如果成员变量是指针,也可以达到这种效果,但是如果是变量就不行了。
<3>引用的特点
int a=10,c=20;
int& b=a;
//下面的例子:修改b的值就是修改a的值,并没有改变b是a别名的事实,只是改变了a或者b的值。
b=12;
//b=c;//错误。已经是a的别名了,不能再修改称为c的别名了
<4>为何使用引用?
<5>引用的作用

取代指针:都可以通过函数修改函数外部的值。

有时使用引用而不是指针:指针可能是空的,会吐核。

<6>引用和指针的区别
    //各种类型指针的大小(32位的编译器是4,64位的是8)
    int* pi =&i;
    char* pc=&c;
    bool* pb=&b;
    float* pf=&f;
    double*pd=&d;
    cout << "sizeof(pi):" << sizeof(pi) <<endl;
    cout << "sizeof(pc):" << sizeof(pc) <<endl;
    cout << "sizeof(pb):" << sizeof(pb) <<endl;
    cout << "sizeof(pf):" << sizeof(pf) <<endl;
    cout << "sizeof(pd):" << sizeof(pd) <<endl;
    //各种引用的大小
    int& ri=i;
    char& rc=c;
    bool& rb=b;
    float& rf=f;
    double& rd=d;
    cout << "sizeof(ri):" << sizeof(ri) <<endl;//4
    cout << "sizeof(rc):" << sizeof(rc) <<endl;//1
    cout << "sizeof(rb):" << sizeof(rb) <<endl;//1
    cout << "sizeof(rf):" << sizeof(rf) <<endl;//4
    cout << "sizeof(rd):" << sizeof(rd) <<endl;//8

引用类型的成员变量在计算类/对象的sizeof和单独计算时是不同的。

class Reference{
    int& ri;
    char& rc;
    bool& rb;
    float& rf;
    double& rd;
public: 
    Reference(int& i,char& c,bool& b,float& f,double& d):ri(i),rc(c),rb(b),rf(f),rd(d){}
    void PrintSize(){
        cout << "sizeof(ri):" << sizeof(ri) <<endl;//4
        cout << "sizeof(rc):" << sizeof(rc) <<endl;//1
    }
};
sizeof(Reference)=20;

(6)拷贝/复制构造函数

<1>语法
类名(类名& 形参){ 
    函数体
}
或者:
类名(const 类名& 形参){ 
    函数体
}

只有拷贝构造函数必须使用引用,其他所有函数使用引用只是避免对象传参时多余的拷贝构造。不使用引用会调用拷贝构造函数,但是该函数不使用引用,自己没法调用自己啊。

<2>调用时机

1.手动调用

类名 对象名;  // 调用默认构造函数
类名 对象2 = 对象1;    // 调用复制构造函数
类名 对象3(对象1);     // 调用复制构造函数

2.自动调用

<3>实例
#include <iostream>
#include <cstring>
using namespace std;
class Array{
public:
    int* data;
    int size;
public:
    Array():data(NULL),size(0){
        cout << "Array Default Construction" << " this:" << this << " &data:"<< data <<endl;
        
    }
    Array(const Array& arr){//拷贝/赋值构造函数(也可以写成初始化列表的形式)
        cout << arr.data << endl;
        data=new int[arr.size];
        memcpy(data,arr.data,sizeof(int)*arr.size);//int类型可以这样拷贝
        size=arr.size;
        cout << "Array Copy Construction:" <<" this:" << this << " &data:"<< data <<endl;
    }
    ~Array(){
        cout << "Array Destruction Free address:" <<" this:" << this << " &data:"<< data << endl;
        delete [] data; 
        data=NULL;
        size=0;
    }
    void push_back(int val){
        ++size;
        int* tmp=new int[size];
        memcpy(tmp,data,sizeof(int)*(size-1));
        tmp[size-1]=val;
        delete [] data;
        data=tmp;
    }
    int get_size(){
        return size;
    }
    int& at(int index){
        return data[index];
    }
};
//在arr前面加上&可以解决,否则就会调用拷贝构造函数,
//来隐式建立一个新的对象,并在对象作用域结束的时候调用析构函数来销毁新建的对象。
void PrintArray(Array arr){//4.copy(对象d),free(d)
    for(int i=0;i<arr.get_size();++i){
        cout << arr.at(i) << endl;
    }
}
Array CreateArr(){
//不加构造函数编译可以通过,由于没有初始化分配内存会导致吐核。
    Array arr;//1.default(对象a)
    arr.push_back(100);
    arr.push_back(200);
    arr.push_back(300);
    return arr;//2.copy(对象b),free(a)
}
int main(){
    Array arr=CreateArr();//3.copy(对象c),free(b)
    for(int i=0;i<arr.get_size();++i){//修改返回值(+10)
        int& t=arr.at(i);//必须使用引用来接返回值
        t +=10;
    }
    PrintArray(arr);
}//5.free(c)

class BigObject{};
BigObject foo(){return BigObject(); // RVO(匿名对象)}
BigObject bar(){
    BigObject localObj;
    return localObj; // NRVO(有名字的对象)
}

gcc/clang自动RVO/NRVO优化,不执行拷贝构造函数,所以在执行时看不到执行了拷贝构造函数。当不忽略拷贝构造函数,可以在编译命令添加选项禁止-fno-elide-constructors;
VC在Debug环境下返回值执行拷贝构造函数,在Release环境下实施RVO/NRVO优化。

<4>默认拷贝构造函数

如果没有自己定义拷贝构造函数,编译器会调用默认的拷贝构造函数,和之前不一样的是(默认构造函数和默认析构函数不会做任何事情),但是默认的拷贝构造函数会执行浅拷贝,即只拷贝内存地址.

<5>什么时候需要自己定义拷贝构造函数

深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
假设B复制了A,修改A的时候,看B是否发生变化:

深拷贝(Memberwise Copy)与浅拷贝(Bitwise Copy):


(7)赋值运算符重载函数

<1>语法
类名& operater=(const 类名& 形参){
  // 赋值操作
  return *this;
}

只有拷贝构造函数必须使用引用,其他所有函数使用引用只是避免对象传参时多余的拷贝构造

<2>调用时机
<3>默认赋值运算符重载函数
<4>理解

初始化和赋值的区别:
1.普通情况下,初始化和赋值好像没有什么特别去区分它的意义。int a=100;和int a;a=100之间仿佛没有任何区别,但是对于c++的类就不同了:初始化不是赋值,初始化是创建变量的时候赋予其一个初始值,而赋值的含义是把对象的当前值擦除,用一个新值替代。
2.本质:赋值和初始化的区别:Simple s;Simple t;t=s;(赋值操作)。赋值是两个对象都已经构造好了,初始化是其中一个对象还不存在。
3.出现的问题:
如果类中有指针的话,赋值操作还是会造成二次释放(浅拷贝,调用了二次析构函数),记住赋值操作不会调用拷贝构造函数,因为对象都已经存在不需要再重新构造。class k=t才会调用拷贝构造函数。

<5>实例
class Test{
private:
    int* p;
public:
    Test():p(NULL){//默认构造函数
        cout <<this<< " Default Construction: "<< p<<endl;
    }
    Test(int n){//构造函数重载
        p=new int(n);
        cout<<this <<" Construction: "<<p<<endl;
    }
    Test(const Test& s){//拷贝构造函数
        p=new int(*(s.p));
        cout << this<<" Copy Construction: "<<p<<endl;
    }
    Test& operator=(const Test& s){//赋值运算符重载函数
        if(this==&s) return *this;//1.判断是否是自身赋值
        if(NULL!=this->p){//2.删除之前申请的内存(防止之前的对象赋值过),防止内存泄漏
            delete p;
        }
        p=new int(*(s.p));//深拷贝
        cout << this<<" Operator: "<<p<<endl;
        return *this;//3.返回当前对象
    }
    ~Test(){//析构函数
        cout << this<<" Destruction: "<<p<<endl;
        delete p;
    }
};
int main(){
    Test s(10);//重载
    //初始化操作
    //Test s1=s;//拷贝
    Test s2;//默认
    //赋值操作不是初始化
    s2=s;//赋值运算符重载函数
//(对于类来说,写不写赋值构造函数都一样),上面的等式可以写成:s2.operate=(s);
//基本类型是没有这种函数的。
}

对于类中的成员变量是非指针时,写不写赋值运算符都一样。

<6>赋值运算符重载的禁用

和拷贝构造函数的禁用一样!!!
c++98和c++11两种。

//(1)c++98
private:
      类名& operator=(const 类名& 对象形参);//并且不在函数中实现
//(2)c++11
public:
     类名& operator=(const 类名& 对象形参)=delete;
<7>三大定律(Rule of three/The Big Three)

如果类中明确定义下列其中一个成员函数,那么必须连同其他二个成员函数编写至类内,即下列三个成员函数缺一不可:

(8)友元细说

友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。

<1>作用

非成员函数访问类中的私有成员

<2>分类
//(1)在类中只写【friend 函数声明】,不写其实现,实现在类外面。
class 类名{
public:
    friend void Func(参数);
};
void Func(参数){
    函数体;
}
//(2)在类中直接完成全局函数的实现,在函数名前面加上friend。
class 类名{
public:
      firend void Func(参数){
            函数体;
      }
};
class 类名1{
public:
        friend 类名2;
};

需要分为.h和.cpp(声明和实现)两个文件来编写,有5个文件(两个类的声明,两个类的实现,及main函数)。
注意:(1)如果一个文件需要知道类的成员信息,要include头文件
(2)如果一个文件只需要只知道类名,可以使用类的前置声明.
如果用反或者多用的话都会报错。

<3>特点

(9)const限定符

<1>本质
<2>const与变量和对象
(1)const 类型 变量 = 初始值;
或者:类型 const 变量 = 初始值;//一般采用这种写法,const在类型的后面
int const a=2;
(2)类型 const 对象;
<3>const与指针
<4>const与引用
类型 const& 变量=初始值
const 类型& 变量=初始值
int a=10;
const int& b=a;
b=20;//错误
a=20;//可以

引用对象的值不可以改变。

<5>const与函数的参数和返回值
void Func(const Simple& s){
    s.Print();
}
(1)不加引用会导致调用拷贝构造函数(默认浅拷贝),造成多次释放,所以加&
(2)不加const只能接受非const成员,不能接受const成员和匿名成员,
(这就是为什么拷贝构造函数的参数要加const的原因,因为不知道传入的参数是什么)
int main(){
    Func(s);//非const对象
    Func(k);//const对象
    Func(Simple(100));//匿名对象
}
//在上面的Simple类中编写成员函数
const  int Get(){//这种情况是没有意义的,因为copy的一份返回值赋值给a。
    return n;
}
int a=s.Get();
*********************************
const在字符串和指针中的使用
//在String类中编写成员函数
const char* c_str(){
    return str;
}
char* s=s.c_str();//不能接收,因为返回值是const char*。
const char* s=s.c_str();//必须使用const char* 来接收,返回值是一个常量指针。
<6>const修饰的成员变量和成员函数
class Simple{
    int m=0;//C++98不允许成员变量在类声明中直接初始化
    int const n=0;
private:
    Simple():n(0){}//const成员变量必须在初始化列表中初始化
    Simple(int n):n(n){}  
};
int main(){
Simple s;
Simple t=s;//拷贝构造,可以使用拷贝构造函数
t=s;//如果存在const成员变量,不能使用赋值运算符
}

(3)注意:在c++11中成员变量在类定义中有初始化,并且在构造函数中也有初始化,编译器会自动忽略类定义中的初始化。
(4)应用:const成员变量一般用于类定义后不可修改的信息,例如:学生学号

使用const成员变量不能省略构造函数(引用类型的成员变量相同)
使用const成员变量不能使用赋值运算符重载函数

class Simple{
    int n;
public:
    Simple(int n):n(n){}
    void Print()const{//const成员函数,const成员函数
        cout << n << endl;
    }
    void Increase(){//非const的成员函数
        ++n;
    }
};
int main(){
    Simple s(10);//定义非const对象
    s.Increase();//非const对象可以调用非const成员函数
    s.Print();//非const对象也可以调用const成员函数
    const Simple k(20);//定义const对象
    k.Print();//const对象可以调用const成员函数
    //k.Increase();//const对象不能调用非const成员函数
}

(1)全局函数是不能被const修饰的,const只能修饰成员函数
(2) 必须在成员函数的声明和定义后都加上const

<7>const修饰总结

(10)static限定符

<1>本质
<2>语法
class 类名{
     //静态成员变量
    static 类型  变量;
    //静态成员函数
    static 返回类型 函数(形参列表);//在返回类型前加static
};
 //静态成员变量
类型  类名::变量=赋值;///静态成员变量必须在类外面定义初始化,且没有static
//静态成员函数
返回类型 类名::函数(形参列表){//注意:定义函数时没有static
    函数体;
}

静态成员变量必须在类外面初始化,且没有static限定符。

(1)通过类名(Class Name)调用:
类名::函数(实参列表);
(2)通过对象(Object)调用:
对象.函数(实参列表);
<3>规则
<4>禁忌
<5>实例
class StaticSimple{
    string name;
    static int num;//行号(1)
    int count;//记录对象说了多少句话
public:
    StaticSimple(const string& name):name(name),count(0){}//构造函数
    void Print(const string& s){//const不能修饰成员函数,因为count是递增的
      cout << ++num<< " "<<name<<":\""<< s << "\""<< ++count <<endl;//(2)(5)
    }
    static void PrintComment(const string& s){
        cout << ++num<<" "<<s<<endl;//不能使用count(3)
    }
};
// 静态成员必须在类外定义初始化
// static 关键字只能用在静态成员变量的声明前,不能用在定义初始化
/*static*/ int StaticSimple::num = 0;
int main(){
    StaticSimple s("LerBon");
    s.Print("Hello");
    //StaticSimple::Print("Hello");//(4)
    s.PrintComment("Hello World");
    StaticSimple::PrintComment("Hello World");//(4)
    StaticSimple t("Jobs");
    t.Print("Apple");//静态变量在对象之间可以共享数据,num(行号)的自增
    s.Print("What?");
    t.Print("Big Apple");
    s.Print("Ok!");
}
<6>static总结
<7>static const/static const限定符

static const/const static修饰的成员变量在类初始化必须是数字类型(int和char),把char归到数值类型,因为里面存的是ascii码。

class Test{
  static const int a=10;
  static const char e='a';
  //static const float b=3.14;//不行,必须在类外初始化
};
/*static*/ const float Test::b=3.14;//要加const,不能加static,为了规范一般加上省略。
变量类型 声明位置
一般成员变量 在构造函数初始化列表中初始化
const成员常量 必须在构造函数初始化列表中初始化
static成员变量 必须在类外初始化
static const/const static成员变量 变量声明处或者类外初始化
<8> 类和结构体的sizeof

(11) 内联函数

inline -- 宏定义的接班人
编译时将函数体代码和实参代替函数调用语句。

<1>本质
<2>条件
<3>语法
inline void Tickets::Get(){//inline必须写在头文件里面
        --count;
        ++getcount;
        cout << name << " leave " << count << " have " << getcount << endl; 
}
inline /*static*/ void Tickets::UpdateCount(int n){
    count =n;
}
<4>慎用内联
<5>理解

(12)运算符重载

<1>为什么要进行运算符重载

运算符重载是为了解决类对象之间的运算的,通常的运算符只用于算术运算,如常量int之间,因为编译器已经定义了;而一个类的两个对象之间成员进行运算必须重新定义,让编译器在遇到对象运算时能按我们要求的进行运算,这就是运算符重载的意义,即重定义运算符.运算符重载的声明operator 关键字告诉编译器,它是一个运算符重载

<2>语法
返回值类型 operator 运算符(参数){
      函数体
}
friend 返回值类型 operator 运算符(形参列表) { 
      函数体 
} 
<3> 数学类的运算符重载(复数类)
 class Complex{
private:
    int real;
    int imag;
public:
    Complex():real(0),imag(0){}
    Complex(int real):real(real),imag(0){}//单个参数的构造函数可以实现int自动转化成Complex对象
    Complex(int real,int imag):real(real),imag(imag){}
}
void Print(){
        cout << real << "+" << imag << "i" <<endl;
    }
        //算数运算符,成员函数加法
    Complex operator+(const Complex& a)const{//不用返回引用,对局部变量的引用是没有意义的
        Complex res;//不要返回局部变量的引用,可以返回成员变量的引用
        res.real=real+a.real;
        res.imag=imag+a.imag;
        return res;
    }
    //简写
    Complex operator+(const Complex& a)const{
        return Complex(real+a.real,imag+a.imag);//匿名对象
    }
    //友元加法
    friend Complex operator(const Complex& a,const Complex& b){
        return Complex(a.real+b.real,a.imag+b.imag);
    }
测试:
     Complex c1(1,2);
        Complex c2(2,3);
        (c1+c2).Print();//匿名对象
      (c1+1).Print();//1会默认转换成复数
        //c1.operator+(Complex(1)).Print();//成员函数运算符重载
    //operator+(c1,Complex(1)).Print();//友元函数运算符重载
      (1+c1).Print();
        //1.operator+(c1);错误
    //operator+(Complex(1),c1).Print();////友元函数运算符重载
//关系运算符
    bool operator==(const Complex& a)const{//不加const会在后面的友元!=中报错
        return real==a.real && imag==a.imag;
    }
       friend bool operator!=(const Complex& a,const Complex& b){//!=
        return !(a==b);
    }
        cout << (c1!=c2) << endl;
        //c1.operator!=(c2);
    //opearator!=(c1,c2);
    //c1.operator!=(c2,X);//不写friend表示是类的成员函数,会用.调用,
//单目算数运算
    Complex operator+(){//取正
        return *this;
    }
    Complex operator-(){//取反
        return Complex(-real,-imag);
    }
//前缀自增运算
    Complex operator++(){//前缀++
        ++real;
        return *this;
    }
        friend Complex operator++(Complex& a){
                ++a.real;
                 return a; 
}
//后缀自增运算
    Complex operator++(int){//后缀++
        Complex res=*this;
        ++real;
        return res;
    }
friend Complex operator++(Complex& a,int){//(这个是局部变量使用引用不能返回)
        Complex res=a;
        ++a.real;
        return res;
    }
<4>流运算符重载

流运算符只能使用友元函数实现,因为函数的第一个参数必须是流对象,不是类创建的对象。一般在流运算符中不加endl,在外面控制回车。

    //流运算符
    friend ostream& operator<<(ostream& os,const Complex& c){
        os << c.real << '+' << c.imag<<'i';
        return os;
    }
    friend istream& operator>>(istream& is,Complex& c);//没有const,因为c是改变的
};
istream& operator>>(istream& is,Complex& c){
    is >> c.real >> c.imag;
    return is;
}
测试:
    cin >> c;//等同于 operator>>(cin,c);
    cout << c << endl;

cin和cout中的c是字符character的意思。

<5>中括号[ ]运算符重载(下标运算符重载)

标准库中的可以直接使用【】来打印和修改,但是自己定义的类是不能打印和修改的,因此需要重载。只有成员函数

class String{
    char* str;
public:
    //operator[]的返回类型通常为引用,否则返回值是不能修改的,因为他是一个常量,不是变量。
      //返回值不加引用可以打印,但是不能修改其值。
    char& operator[](int index){//返回值类型由成员变量的类型决定。
        return str[index];
    }
};
int main(){
    String s2("hello");//自己定义的类必须重载[ ]
    s2[0]=toupper(s2[0]);
    cout << s2[0] << endl;
}
<6>小括号()的重载(函数调用运算符重载)
class Simple{
    int n;
public:
    Simple(int n):n(n){}
    void operator()(int a){
        cout << (n+a) << endl;
    }
};
int main(){
    Simple s(10);
    s(12);//仿函数,像函数
        //s.operator()(12);
输出:22
}
<7>运算符重载的规则

(13)构造函数形参

<1>构造函数形参的默认值
class Simple{
    int n;
public:
    //Simple():n(0){}
    Simple(int n=0):n(n){}//加默认参数,可以使Simple t有意义,在不写其他构造函数时不会报错。
    void operator()(int a){
        n+=a;
    }
    friend ostream& operator<<(ostream& os,const Simple& s){
        os<<s.n;
        return os;
    }
};
int main(){
    Simple t;
    cout << t <<endl;//0
    Simple s(10);
    cout << s << endl;//10
    s(12);//s.operator()(12);
    cout << s <<endl;//12
}
<2>禁止单参默认转换

关键字:explicit
如果构造函数只有一个参数,使用时存在默认转换的情况。如果在单参构造函数前加上关键字explicit,可以禁止默认转换的情况。

class A{
    int n;
public:
    explicit A(int i):n(i){
        cout << "A(" << i << ")" << endl;
    }
    A(const A& m){//拷贝构造函数
        n=m.n;
        cout << "Copy Construct" << endl;
    }
    A operator+(A b){
        return A(n + b.n);//A(30) A(30)
    }
};
int main(){
    A a(10);  //A(10)
    A b(20);//A(20)
    
        //错误
    //1+a;//1.operator+(a);
    //A operator+(const A&,const A&);//如果写的是加法的友元函数重载就可以

        //下面两个会调用拷贝构造函数
        a.operator+(b);
    a+b;

      //在不加关键字时,下面是对的,1会默认转换为A(1)为匿名对象,不会调用拷贝构造函数。
    //a+1;
    //a.operator+(A(1));//默认转换
      
    //在不加explicit时,100会自动转换为A(100),下面等式可以理解为A c=A(100),不会调用拷贝构造函数。
        A c=100;    
}
因此:匿名对象初始化一个对象时,是不会调用拷贝构造函数的。
上一篇下一篇

猜你喜欢

热点阅读