自学内容网 自学内容网

C++学习-->类和对象

目录

一、类的定义

1、类定义格式

2、访问限定符

3、类域

二、实例化

1、概念

2、对象大小

内存对齐规则

三、this 指针

四、默认成员函数

1、构造函数

构造函数的特点:

初始化列表: 

2、析构函数

析构函数的特点:

3、拷贝构造函数

拷贝构造的特点:

4、赋值运算符重载

1)运算符重载

2)赋值运算符重载

特点:

5、const成员函数

五、类型转换

六、static成员

七、友元

八、内部类

九、匿名对象


一、类的定义

1、类定义格式

  • class 作为类的关键字,在关键字后加上类的名字,{} 中为类的主体,且类定义结束后的分号不能省略。
  • 类中的内容作为类的成员:类中的变量作为类的属性或成员变量、类中的函数称为类的方法或成员函数。
  • 为区分成员变量,一般会在成员变量上加上特殊标识,如成员变量面前或后面加  _  或 m 开头。
  • 在C++中 struct 也可以定义类,明显的变化是 struct 中也可以定义函数,但一般情况下还是推荐 class 定义类。
  • 类的成员函数默认为内联函数( inline )。
    class test01
    {
        int _x;
        int _y;
    
        int Add(int a,int b)
        {
            return a+b;
        }
    };
    
    struct test02
    {
        int _x;
        int _y;
    
        int Add(int a,int b)
        {
            return a+b;
        }
    }

    2、访问限定符

  • 访问限定符是C++实现封装的方式,通过访问权限选择性的将接口提供给外部的用户使用
  • public 修饰的成员在类外可以被直接访问;protected 和 private 修饰的成员在类外不可以被直接访问
  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到  }  即类结束
  • class定义成员没有被访问限定符修饰时默认为 private ,struct 默认为 public 
  • 一般成员变量都会被限制为 private / protected ,需要给别人使用的成员函数会被设置为 public 
class test01
{
private:
    int _x;
    int _y;
public:
    int Add(int a,int b)
    {
        return a+b;
    }
};

3、类域

  • 类定义了一个新的作用域,类的所有成员都在类的作用域中,在类外定义成员时,需要使用   :: 作用域操作符指明成员属于哪个类域
    class test01
    {
    private:
        int _x;
        int _y;
    public:
       
    };
    int test01::Add(int a,int b)
    {
        return a+b;
    }

二、实例化

1、概念

  • 用类类型在物理内存中创建对象的过程,称为实例化出对象

  • 类时对象进行的抽象描述,是一个模型,限定类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间

  • 一个类可以实例化出多个对象,实例化的对象占用实际的物理空间,储存类的成员变量。类似按设计图建房子,只有建好的房子才是占用实际的物理空间,存储成员变量 

//类
class test01
{
private:
    int _x;
    int _y;
public:
   
};
int test01::Add(int a,int b)
{
    return a+b;
}

int main()
{
    //实例化
    test01 t;
    return 0;
}

2、对象大小

类实例化出的每个对象,都有独立的数据空间,对象中包括了成员变量,而成员函数被编译后是一段指令,对象中没办法存储,这些指令会单独存在一个单独的区域(代码段)

内存对齐规则

由于成员函数在不存储在实例化出的类对象中,所以需要内存对齐的只有类对象中的成员变量,所以内存对其规则和结构体的对齐规则相同,具体看C语言学习—结构体-CSDN博客

  • 第一个成员在与结构体偏移量为 0 的地址处
  • 其它成员要对其到某一个数字(对齐数)的整数倍的地址处
  • 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(即对齐数和该成员大小之中选最小)
  • 结构体总大小为:最大对齐数的整数倍
  • 如果嵌套了结构体,嵌套结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

三、this 指针

编译器编译后,类的成员函数默认都会在刑参第一个位置,增加一个当前类类型的指针,叫做   this 指针

class Date
{
private:
    int _year;
    int _month;
    int _day;
public:
    void Init(int year = 2000, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
};

例如这里 Date 类的 Init 成员函数的真正原型

    void Init(Date* const this, int year = 2000, int month, int day)

类的成员函数中访问成员变量都是通过 this 指针访问的,例如 Date 类的 Init 成员函数给成员变量赋值

    void Init(Date* const this, int year = 2000, int month, int day)
    {
        this->_year = year;
        this->_month = month;
        this->_day = day;
    }

C++规定不能再实参和形参的位置显示的写 this 指针(因为编译器会处理),但是可以子啊函数体内显示的使用 this 指针

    void Init(int year = 2000, int month, int day)
    {
        this->_year = year;
        this->_month = month;
        this->_day = day;
    }

四、默认成员函数

1、构造函数

构造函数是在对象实例化时用于初始化对象的函数,且在实例化对象时构造函数会自动调用,不需要我们主动调用

构造函数的特点:

  • 函数名与类名相同
  • 无返回值(也不需要写 void )
  • 对象实例化时会自动调用对应的构造函数
  • 构造函数可以重载
  • 如果类中没有显示定义构造函数,那么编译器会自动生成一个无参的默认构造函数
  • 无参构造函数,全缺省构造函数以及编译器默认生成的构造函数,都叫默认构造函数。这三个函数有且只有一个存在,不能同时存在,如无参构造函数和全缺省构造函数虽然构成重载,但是会在调用时构成歧义
  • 编译器默认生成的构造,对内置类型成员变量的初始化没有要求,即是否初始化看编译器。对于自定义成员变量,如果没有默认构造函数,就会报错,要初始化自定义成员变量,需要用到初始化列表(内置类型指 int /char / double 等语言提供的原生的数据类型,自定义类型指用 class / struct 等关键字自己定义的类型)

例如:定义一个日期类型的构造函数

class Date//类
{
private:
int _year;
int _month;
int _day;
public:
//构造函数
Date(int year = 1900, int month = 1, int day = 1);
//判读月份是否在范围内
bool CheckDate();
//月份天数
int GetMonthDay(int year, int month);
};

//构造函数
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;

if (!CheckDate())
{
cout << "日期非法->";
}
}

//月份天数
int Date::GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthdayarr[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
//判断闰年
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 29;
}
else
{
return monthdayarr[month];
}
}

//判读月份是否在范围内
bool Date::CheckDate()
{
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}

初始化列表: 

  • 上面实现构造函数时,使用的是函数体内赋值,构造函数还有一次方式,就是初始化列表,初始化列表是以一个冒号开始,以一个逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式
//构造函数
Date::Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{
if (!CheckDate())
{
cout << "日期非法->";
}
}
  • 引用成员变量,const 成员变量,没有默认构造的类类型变量,必须放在初始化列表进行初始化
  • C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的
class Date//类
{
private:
int _year;
int _month;
int _day = 1;
public:
//构造函数
Date(int year = 1900, int month = 1);
//判读月份是否在范围内
bool CheckDate();
//月份天数
int GetMonthDay(int year, int month);
};
//构造函数
Date::Date(int year, int month)
:_year(year)
,_month(month)
{
if (!CheckDate())
{
cout << "日期非法->";
}
}
  • 初始化列表按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关,建议声明顺序和初始化列表顺序保持一致

2、析构函数

析构函数与构造函数功能相反,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作

析构函数的特点:

  • 析构函数名是在类名面前加上字符  " ~ " 
  • 无参数无返回值(不需要加上 void)
  • 一个类只能有一个析构函数,若未显示定义,系统会自动生成默认的析构函数
  • 对象生命周期结束时,系统会自动调用析构函数
  • 跟构造函数泪水i,编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数
  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器自动生成的默认析构函数,如日期类,如果有资源申请时,一定要写析构,否则会造成资源泄露
class myclass
{
private:
    int* _arr;
public:
    //构造
    myclass()
    {
        _arr = (int*)malloc(sizof(int)*10);
    }

    //析构
    ~myclass()
    {
        free(_arr);
        _arr = NULL;
    }
}
  • 一个局部域的多个对象,C++规定后定义的先析构

3、拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫拷贝构造函数,拷贝构造是一个特殊的构造函数

拷贝构造的特点:

  • 拷贝构造是构造函数的一个重载
  • 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器会报错,语法逻辑上会引发无限递归调用。拷贝构造可以有多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值
class Date//类
{
private:
int _year;
int _month;
int _day;
public:
//构造函数
Date(int year = 1900, int month = 1, int day = 1);
//判读月份是否在范围内
bool CheckDate();
//月份天数
int GetMonthDay(int year, int month);
};


//构造函数
DateDate(int year, int month, int day = 1)
:_year(year)
,_month(month)
    ,_day(day)
{
if (!CheckDate())
{
cout << "日期非法->";
}
}

//第一个参数不是类类型对象的引用
Date::Date(Date d)
{
    _year = d.year;
    _month = d.month;
    _day = d.day;
}


int main()
{
    Date d1(2024,9,1);
    
    Date d2(d1);
    return 0;
}

  • C++规定自定义类型对象进行拷贝行为是必须调用拷贝构造,所以自定义类型传值传参和传值返回都会调用拷贝构造完成
  • 若没有定义拷贝构造,编译器会生成自动生成的拷贝构造函数,自动生成的拷贝构造函数对内置类型成员变量会完成浅拷贝,对自定义类型成员变量会调用它的拷贝构造
  • 对于全是内置类型且没有内置类型指向申请的空间,那么编译器自动生成的拷贝构造就可以完成需要的拷贝,但要是有内置类型成员指向申请的空间,那么浅拷贝就不满足拷贝的需求,需要自己写拷贝构造函数,实现深拷贝
  • 传值返回会产生一个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名,没有产生拷贝,但是如果返回对象是以当前局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于野引用,类似一个野指针引用。传引用返回可以减少拷贝,但是要确保对象在当前函数结束后不会被销毁

4、赋值运算符重载

1)运算符重载

  • 当运算符被用于类类型对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,会报错
  • 运算符重载是有特殊名字的函数,名字由 operator 和后面要定义的运算符共同构成,和其他函数一样,也具有返回类型和参数列表以及函数体
  • 重载运算符的参数个数和该运算符作用的运算对象一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
  • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符作为成员函数时,参数比运算对象少一个
  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一种
  • 不能通过连接语法中没有的符号来创建新的操作符
  • (.*) (::) (sizeof) (?:) (.) 这五个操作符不能重载
  • 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义
  • 一个类需要重载哪些操作符,是看哪些运算符重载后有意义,比如Date类日期相减有意义,但日期相加没意义,所以重载 " - " 有意义 ,但重载 "  +  " 没意义
  • 重载++ 运算符时,有前置++和后置++,所以规定重载后置++时增加一个 int 形参便于区分,即operator++(int)

例:重载对于Date类的运算符

class Date//类
{
public://公有
//构造函数
Date(int year = 1900, int month = 1, int day = 1);
//月份天数
int GetMonthDay(int year, int month);
//判断月份是否在范围内
bool CheckDate();
//运算符重载
bool operator<(const Date& d);
bool operator>(const Date& d);
bool operator<=(const Date& d);
bool operator>=(const Date& d);
bool operator==(const Date& d);
bool operator!=(const Date& d);
Date& operator+=(int day);
Date operator+(int day);
Date& operator-=(int day);
Date operator-(int day);
//++d
Date& operator++();
//d++
Date& operator++(int);
//--d
Date& operator--();
//d--
Date& operator--(int);
//日期-日期
int operator-(const Date& d);
private://私有
int _year;
int _month;
int _day;
};
//构造函数
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;

if (!CheckDate())
{
cout << "日期非法->";
cout << *this;
}
}

//月份天数
int Date::GetMonthDay(int year, int month)
{
assert(month > 0 && month < 13);
static int monthdayarr[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
//判断闰年
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 29;
}
else
{
return monthdayarr[month];
}
}

//判读月份是否在范围内
bool Date::CheckDate()
{
if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month))
{
return false;
}
else
{
return true;
}
}

//运算符重载
bool Date::operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if(_year == d._year && _month < d._month)
{
return true;
}
else if(_year == d._year && _month == d._month && _day < d._day)
{
return true;
}
return false;
}
bool Date::operator>(const Date& d)
{
return !(*this <= d);
}
bool Date::operator<=(const Date& d)
{
return *this == d || *this < d;
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
bool Date::operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}

_day += day;
while (_day > GetMonthDay(_year,_month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}

_day -= day;
while (_day <= 0)
{
--_month;
if (_month <= 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
//++d
Date& Date::operator++()
{
*this += 1;
return *this;
}
//d++
Date& Date::operator++(int)
{
Date tmp;
*this += 1;
return tmp;
}
//--d
Date& Date::operator--()
{
*this -= 1;
return *this;
}
//d--
Date& Date::operator--(int)
{
Date tmp;
*this -= 1;
return tmp;
}
//日期-日期
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
min++;
n++;
}
return n * flag;
}

2)赋值运算符重载

赋值运算符是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象

特点:
  • 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成
    const当前类类型引用,否则会传值传参会有拷贝
  • 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值⽬的是为了⽀持连续赋值场景。
  • 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对⾃定义类型成员变量会调用他的赋值重载函数
  • 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动⽣成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现赋值运算符重载。

5、const成员函数

  • 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后 面。
class Date
{
public:
    void Print() const
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
    //即 void Print(const Date* const this) const
}

const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this

五、类型转换

  • C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数
class A
{
public:
     A(int a1)
         :_a1(a1)
     {

     }
private:
    int _a1 = 0;
}

int main()
{
    A a = 1;
    return 0;
}
  • 构造函数前⾯加explicit就不再⽀持隐式类型转换。
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。

六、static成员

  • 用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。
  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
  • 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
  • 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
  • 突破类域就可以访问静态成员,可以通过  类名::静态成员  或者  对象.静态成员  来访问静态成员变量和静态成员函数。
  • 静态成员也是类的成员,受public、protected、private访问限定符的限制。
  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。

七、友元

  • 友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类 声明的前面加  friend ,并且把友元声明放到⼀个类的里面。
  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
  •  友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
  • ⼀个函数可以是多个类的友元函数。
  • 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
  • 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
  • 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
  • 友元提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

例如:对Date类的  " << " 和 " >> " 运算符的重载

这两类的运算符的第一个形参必定是该运算符的 io流,而类类型成员函数第一个形参是该类隐藏的 this 指针,所以 " << " 和 " >> " 运算符不能重载为类类型的成员函数,只能作为类类型的友元函数使用

class Date//类
{
//友元函数声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(iostream& in, Date& d);
private://私有
int _year;
int _month;
int _day;
};

// <<符重载
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
// >>符重载
istream& operator>>(iostream& in, Date& d)
{
while (1)
{
cout << "请依次输入年月日:>";
in >> d._year >> d._month >> d._day;
if (d.CheckDate())
{
break;
}
else
{
cout << "日期非法,请重新输入" << endl;
}
}
return in;
}

八、内部类

  • 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在 全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
  • 内部类默认是外部类的友元类。
  • 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。

九、匿名对象

  • 用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象
  • 匿名对象⽣命周期只在当前⼀行,⼀般临时定义⼀个对象当前用⼀下即可,就可以定义匿名对象。
class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        cout << "A(int a)" << endl; 
    }
    ~A()
    {
        cout << "~A()" << endl;
    }
    void print()
    {
        cout << _a << endl;
    }
private:
    int _a;
}

int main()
{
    A();
    A(1);
    A(2).print();
    return 0;
}


原文地址:https://blog.csdn.net/weixin_47298551/article/details/142037154

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!