c++类与对象详解

1,结构体到类过渡

c++兼容c语言,结构用法可以继续使用,同时struct也升级成了类。既可以用结构体的方式去访问成员变量,也可以用类的的方式去访问成员变量。

struct Stack {
    //成员变量
    int* a;
    int top;
    int capacity;
    //成员函数
    void Init() {
        a = nullptr;
        top = capacity = 0;
    }
    void push(int x) {

    }
};

2,类的定义

(1)类的定义:

class 类名{主体},主体包含成员变量,和成员函数

(2)访问限定符(三种):

public(共有),protected(保护),private(私有)

访问限定符一般作用到下一个访问限定符结束或者遇到“}”类结束

在类中,成员的默认访问限定符是私有的,所以如果不写访问限定符,类中的成员函数是无法使用的。而在结构体中,成员的默认访问限定符是共有的。

(3)类的声明和定义可以分离:

声明在.h 的头文件里,成员变量与之前的写法相同,但是成员函数,写成函数声明的形式

例如:

stack.h//声明部分

class Stack {
private:
    int* a;
    int top;
    int capacity;

public:
    void Init();
    void push(int x);
    int Top();

    
};

定义部分在源文件中书写,要包头文件,这时,只需要定义成员函数,注意:在成员函数前面加“ 类名+命名空间限定符”(原因:需要在类域中寻找变量)

#include"stack.h"
#include <exception>
void Stack::Init() {
    a = nullptr;
    top = capacity = 0;
}
void Stack::push(int x) {
    if (top == capacity) {
        size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
        a = (int*)realloc(a, sizeof(int) * newcapacity);//realloc申请空间
        capacity = newcapacity;
    }
    a[top++] = x;
}
int Stack::Top() {
    return a[top - 1];
}

注意:直接在类里面定义,会默认为内联函数,不会生成符号表,因而可以写成以下这种形式。(注意:普通的函数,声明和定义一定要分离)

void Init() {
    a = nullptr;
    top = capacity = 0;
}
void push(int x);
int Top();

(4)类的实例化

类中的成员变量相当于声明部分,并没有分配空间,这时就需要定义开空间,也称为对象的实例化。成员变量会在对象中存储一份,但成员方法在公共代码区中。

(5)类的大小

class Date1{//类不占内存的空间
private:
    int _year;//声明
    int _month;
    int _day;
public:
    void Inti(int year, int month, int day) {
         _year = year;
         _month = month;
         _day = day;
    }

};

Date d;

cout << sizeof(Date) << endl;//12
cout << sizeof(d) << endl;//12

类的大小或者说对象的大小看的是成员变量的类型和个数,且遵循内存对齐规则,所以上述对象的大小为12(3*4)

例1:

class A {//4byte
    int a;
    void add() {
    }
};

例2:

class A1 {//内存对齐规则、8byte
    int a;
    char b;
    void add() {
    }
};

例3:class B {//1byte表示占位表示对象存在过
    void add() {
    }
};

class C {//1byte ,D为内部类,和D类受C类域,和访问限定符的限制,但其实他们是两个独立的类

classD{

int a;

}
};

(6)this指针:

编译器在调用成员函数是,会有一个隐藏的this指针

例如:

void Inti(int year, int month, int day) {
      _year = year;
       _month = month;
       _day = day;
    }

Date d;
d.Inti(2024, 9, 9);

而编译器处理后为:

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

Date d;
d.Inti(&d,2024, 9, 9);

但是自己写代码时,不能写成把this作为参数传递,但在类里面可以使用,比如: this->_year = year;这样的写法是可以的

总结:this在实参和形参位置不可以显示的写,但是在类里面可以显示的用,this作为形参存在栈里面,vs里面存在寄存器里面

3,构造函数

构造函数:特殊的成员函数

(1)构造函数的特点:

1,   函数名与类名相同
2,无返回值
3,对象实例化时,自动调用
4,构造函数可以重载

例1:class Date {//类不占内存的空间
private:
    int _year;//声明
    int _month;
    int _day;

public:
    //无参数的构造函数
    Date() {
        _year = 1;
        _month = 1;
        _day =1;
    }

//有参数的构造函数

    Date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    */
   
};

(2)利用缺省参数来实现构造函数:

类中的两个构造函数两者结合一下(缺省参数);
    Date(int year=1, int month=1, int day=1) {
        _year = year;
        _month = month;
        _day = day;
    }

含有缺省参数的函数,声明和定义可以分开写,但声明部分要写成缺省写形式,但定义需要

例2:class Stack {
private:
    int* a;
    int top;
    int capacity;

public:
    Stack() {
        a = nullptr;
        top = capacity = 0;
    }

   
    void push(int x) {
        if (top == capacity) {
            size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
            int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
            if (tmp == nullptr) {
                perror("realloc fail");
                exit(-1);
            }
            if (tmp == a) {
                cout << capacity <<"本地扩容"<< endl;
            }
            else {
                cout << capacity << "异地扩容" << endl;
            }
            a = tmp;
            capacity = newcapacity;
        }
    
        a[top++] = x;
    }

在这种明确知道自己想要的空间且空间较大的情况下,会不断扩容,本地扩容还好,异地扩容会非常的复杂,未了避免这种情况,我们可以写成以下这种代码

Stack s1(1000);
for (int n = 0;n < 1000;n++) {
    s1.push(n);
}

Stack(size_t n=4) {
    if (n == 0) {
        a = nullptr;
        top = capacity = 0;
    }
    else {
        a = (int *)malloc(sizeof(int) * n);
        if (a == nullptr) {
            perror("realloc fail");
            exit(-1);
        }
        top =  0;
        capacity = n;
    }

}

当你知道自己要的空间大小,这个构造函数可以减少扩容,如果你不传入参数,他会自动调用,并且空间大小初始化为4

(3)补充:malloc / calloc / realloc区别

malloc(size_t  size) ---》int* arr=malloc(40)为申请了40个字节的空间大小,即数组长度为10。

size为我们要申请空间的大小,该值是需要我们去计算的。使用malloc()函数申请后空间的值是随机的,并没有进行初始化。

calloc(size_t count,size_t size) ---- 》int *arr = calloc(10,sizeof(int))

count为我们需要申请空间的块数,size为我们需要申请类型的占的字节大小,并不需要人为的计算空间大小。使用calloc()函数却在申请后,对空间逐一进行初始化,并设置值为0;

语法:指针名=(数据类型*)realloc(要改变内存大小的指针名,新的大小)--------》

int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);

返回值:如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。

(4)注意事项:

1,不写构造函数时,编译器会自动生成,如果写了,编译器就不会提供了
2,内置类型成员不会处理(有些编译器会处理为0)(声明部分可以给缺省值)
3,对于自定义类型的成员才会处理,会去调用这个成员的构造函数,一般情况都需要我们写构造函数,决定初始化数值

4,默认构造函数,只有一个。注意:无参构造函数,全缺省构造函数,自动生成的构造函数,都默认为构造函数
总结:但是如果成员变量都是自定义类型,可以不进行初始化

4,析构函数

(1)析构函数的特点

1,在类名前加 ~
2,没有参数没有返回值
3,只有一个析构函数,若没有显示定义,会自动生成一个默认的析构函数,析构函数不能重载
,内置类型成员不会处理,对于自定义类型的成员才会处理
4,在对象销毁时,自动调用析构函数
    

void Destroy() {
        free(a);
        a = nullptr;
        top = capacity = 0;
    }可以写成以下代码:
    
 
~Stack() {
        free(a);
        a = nullptr;
        top = capacity = 0;
    }

5,拷贝构造:

(1)拷贝构造是构造函数的一个重载形式

(2)拷贝构造的参数只有一个且必须是同类型的对象的引用,使用传值传参会直接报错

(3)我们不写,编译器会自动生成拷贝构造,内置类型采用值拷贝(浅拷贝),自定义类型调用成员变量类型的拷贝

总结:内置类型中Date类不需要写拷贝构造,默认生成就够用了,但是Stack类需要写拷贝构造,默认生成的会出问题(默认生成浅拷贝,需要深拷贝),

传值传参是浅拷贝,当传的值是内置类型,程序可以通过,因为传参时只会在栈上开辟空间,函数结束时,栈空间会自动收回,但如果是自定义类型传参,由于func调用完会自动调用析构,释放a指向的空间,Stack调用完后也还自动调用析构函数,会再次释放a所指的空间,但此时a是野指针不能再次释放,所以程序会崩,所以这时就会自动调用拷贝构造来避免两次析构,但如果拷贝构造采用传值传参的方式来写时,当传参时就会调用拷贝构造,调用后,还需要传值传参,就需要再次调用拷贝构造,再传参,再调用……周而复始形成无穷递归,程序崩掉。

综上所述,在传自定义类型时,不应该写成传值传参,这也是为什么拷贝构造的正确写法是引用传参

浅拷贝:

Date(const Date& d) {
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

 深拷贝:

Stack(const Stack& s) {
    _arr = (int *)malloc(sizeof(int) * s._capacity);
    if (NULL == _arr) {
        perror("申请空间失败");
        return;
    }
    memcpy(_arr, s._arr, sizeof(int) * s._top);
    _top = s._top;
    _capacity = s._capacity;
}

注:void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1

6,运算符重载

(1)特征及注意事项:

1,目的:增强代码的可读性,运算符重载是具有特殊函数名的函数,operator加运算符
2,至少有一个参数是自定义类型
3,用于内置类型的运算符,其含义不能改变
4,作为成员变量重载时,形参的个数比操作数目少一,因为成员函数的第一个参数为隐藏的this
5,不能改变操作符的操作数个数,操作符的操作数有几个,参数就有几个
6,“ :: ”“ sizeof ”“ ? ; ”“ . ”“ .* ”  不能重载

Date d1(2024, 2, 3);
Date d2(2024, 4, 5);
cout << (d1 < d2) << endl;

当比较两个自定义类型时,可以使用运算符重载,比较那个日期大,因为不改变对象的的值所以用const修饰,先比年份,在年份相同的情况下比,月份,在月份相同的情况下比天数。返回bool类型 。

bool operator<(const Date& x1, const Date& x2) {
    if (x1._year < x2._year) {
        return true;
    }
    else if (x1._year == x2._year && x1._month < x2._month) {
        return true;
    }
    else if(x1._year == x2._year && x1._month ==x2._month&&x1._day<x2._day){
        return true;
    }
    else {
        return false;
    }
}

但是一般情况下,类中的成员变量都是用private修饰的,不可以在类外被访问,因此通常情况下,我们要把这函数转变为成员函数,因为作为成员变量重载时,形参的个数比操作数目少一(因为成员函数的第一个参数为隐藏的this)所以改写为以下代码:

bool 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;
    }
    else {
        return false;
    }
}

以此类推我们可以写出各种比较的方法:

bool operator==(const Date& d) {
    return _year == d._year 
        && _month == d._month 
        && _day < d._day;
}
bool operator<=(const Date& d) {
    return *this< d || *this == d;
}
bool operator>(const Date& d) {
    return!(*this <= d);
}
bool operator>=(const Date& d) {
    return !(*this <d);
}
bool operator!=(const Date& d) {
    return !(*this == d);
}

并以此拓展:参数为:自定义类型+内置类型

Date ret1 = d1 + 50;某一天再过x天后是几号

通过这个函数得到一个月的天数:

int GetMonthDay(int year, int month) {
    static int monthArray[13] = { 0,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;
    }
    return monthArray[month];
}

用引用返回,直接将原来的天数进行了修改,
Date& operator+=(int day) {
    _day += day;
    while (_day > GetMonthDay(_year, _month)) {
        _day -= GetMonthDay(_year, _month);
        ++_month;
        //月进位
        if (_month == 13) {
            ++_year;
            _month=1;
        }
    }
    return *this;
}

传值返回,创建了了一个临时的对象,在这个对象上进行计算x天后是几号,而原来的日期并没有发生改变。

Date operator+(int day) {
    Date tmp(*this);
    tmp += day;
    return tmp;
}

Date& operator=(const Date& d) {
    if (this != &d) {//防止自己和自己赋值
    this->_year = d._year;
    this->_month = d._month;
    this->_day = d._day;
    }
    return *this;
} //可以默认生成,但是自定义类型(stack)需要自己写

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 operator-(int day) {
    Date tmp(*this);
    tmp -= day;
    return tmp;
}

(2)前置++和后置++的写法:


//++d
Date& operator++() {
    *this += 1;
    return *this;
}
//d++,加了一个int参数,只是为了和前置++进行区分
Date operator++(int) {
    Date tmp(*this);
    *this += 1;
    return tmp;
}


Date& operator--() {
    *this -= 1;
    return *this;
}
Date operator--(int) {
    Date tmp(*this);
    *this -= 1;
    return tmp;
}

日期-日期=天数
int  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;
}

想要通过<<直接打印对象,这时就需要重写<<.我们一般的书写格式是cout<<d<<endl;所以<<运算符重载需要两个参数,第一个是参数是ostream类型的,第二个参数是Date类型的, 如果在类中书写,只需要写一个参数,且默认第一个参数为this(也就是Date),但是这与我们想要达到的效果相斥,所以我们需要写到类外,从而来手动调节两个参数的位置。又因为我们不改变Date中的值,所以加上const,为减少传参产生的拷贝,所以运用引用传参,cout<<d1<<d2<<endl,我们书写时也会遇到这种情况,这时"d1<<d2"<<左右两边不符合<<运算符重载的参数,为了避免这种情况,我们将“cout<<d1”的返回值设为ostream类型的。

ostream& operator<<(ostream& out, const Date& d) {
    out << d._year << "/" << d._month << "/" <<d._day << endl;
    return out;
}

>>书写方法与上述方法类似

istream& operator>>(istream& in, Date& d) {
    in >> d._year >> d._month  >> d._day ;
    return in;
}

需要注意的是,我们将这两个方法写到了类外,但是,在方法中却调用了类中不可对外访问的成员变量,这时,我们就需要在类中写一个声明,友元声明:

friend ostream& operator<<(ostream& out,const Date& d);
friend istream& operator>>(istream& in, Date& d);

7,友元函数

友元函数
1,友元可以访问私有的和保护的成员,但是不属于类的成员函数
2,友元函数不可以用const访问
3,友元函数可以在类的任何地方声明,不受访问限定符限制
4,可以有多个友元函数
5,友元函数的调用和普通函数的调用原理相同

class A {
public:
    class B {
    private:
        int b;
    };
    void print() const{
        cout << a << endl;
    }
private:
    int a;
};
void f1(const A& aa = A()) {
    aa.print();
}

B类受A类域,和访问限定符的限制,但其实他们是两个独立的

类内部类默认是外部类的友元,所以内部类可以访问外部类的私有成员变量,注意:关系是单向的,不是相互的

8,const

总结:只读函数,内部不涉及修改成员,可以使用const

const Date d1;
d1.print();//报错

Print(const Date*), 但是print接受的为Date* this,相当于将小权限放大了这时需要将printf的权限也放小,改写成:

void print() const{
    cout << _year << " " << _month << " " << _day << endl;
}

这里的const,修饰的是传过来的参数Date* this。

举个例子:

class Stack {
private:
    int* _arr;
    int  _top;
    int _capacity;
public:
    Stack() {
       ……
    }
    Stack(const Stack& s) {
       ……
    }
   
  void func( const Stack& s) {
    for (size_t i = 0;i < s.size();i++) {
        cout << s[i] << " ";
    }
    cout << endl;
}

想要实现s[i],打印的时候不想要改变数组本身的值,所以,要将参数加上const。这时运算符重载的参数const Stack*,所以运算符重载的参数也需要是const修饰,我们返回的时候,返回的是那块空间的别名,为了避免通过返回值改变数组中的数值,所以返回值前要加const,到此就写出来只能读不能修改的运算符重载

const int& operator[](size_t i)const {
    assert(i < _top);
    return _arr[i];
}

但同时我们也需要,既可以读又可以修改的[ ]的运算符重载,所以写成以下代码

int& operator[](size_t i) {
    assert(i < _top);
    return _arr[i];
}//能读,能改

9,取地址运算符的重载

cout << &d1 << endl;

并不需要额外再写,因为编译器会自动生成

日常不需要自己写,有特殊需求时,例如不想被获取到地址时,可以自己书写

10,初始化列表

初始化列表
1,以冒号开始,逗号分割,成员变量后面加一个括号,括号里面放初始值或者表达式
2,尽量使用初始化列表进行初始化,因为不管是否使用初始化列表,成员变量的初始化一定会使用初始化列表
3,引用类型,const,没有默认的构造函数的类,这些必须使用初始化列表才能进行初始化

4,能用初始化列表就用初始化列表,但是有些情况还是需要初始化列表和函数体混着用

class A {
private:
    int _a;
public:
    A(int a)
        :_a(a)
    {}
};
class Date {
private:
    int _year;
    int _month;
    int _day=1;

给初始化列表,如果初始化列表没有显示给值,用缺省值。如果有显示给值,则不会用缺省值
    int& _refi;
    const int _x;//常变量必须在定义的地方初始化
    A a;
public:
    Date(int year, int month, int day,int& i)
        :_year(year)
        ,_month(month)
        ,_x(1)
        ,_refi(i)
        ,a(4)
    {
        _day = day;
    }
  
};

初始化列表,每个成员定义的地方 ,初始化列表进行的顺序跟声明的顺序有关

a(4),这里运用了隐式类型转换

11,隐式类型转换

例如:

例1:A a1=2;

例2:一个类(Seqlist )的成员变量为另一个类(A),这个类A的成员变量只有一个整型
Seqlist s;
A aa(3);
s.push(aa);
相当于s.push(3);

这个过程为:A a1=2,中用2调用构造函数生成一个临时对象,在用这个对象去拷贝构造a1
,编译器会在优化为用2直接构造

但是如果不想将int类型转化为A类型,可以加关键词explicit到A的构造函数前面。

12,匿名对象

 A aa1(2);生命周期在当前局部域
 A(7);匿名对象,生命周期只在这一行,简化代码

所以如果这个对象只需要用一次,就可以用匿名对象,从而简化代码

const引用可以延长匿名对象的生命周期,ref出了作用域,匿名对象就销毁了。

写法为:const A& ref = A();

13,静态成员变量和静态成员函数

int n = 0;//累计创立了多少个对象
int m = 0;//正在使用有多少个对象
class A {
public:
    A() {
        ++n;
        ++m;
    }
    A(const A& t) {
        ++n;
        ++m;

    }
    ~A() {
        --m;
    }
};
A func(A aa) {
    return aa;
}
int main() {
    A aa1;
    A aa2;
    cout << n << " " <<m<< endl;//2 2
    A();
    cout << n << " " << m << endl;// 3 2
    func(aa1);
    cout << n << " " << m << endl;//5 2

}

防止在外部将m,n更改,所以我们将m,n设为静态成员变量

class A {
public:
    A() {
        ++n;
        ++m;
    }
    A(const A& t) {
        ++n;
        ++m;

    }
    ~A() {
        --m;
    }
    //静态成员函数的特点的没有指针
    static int GetM() {
        return m;
    }
     static int GetN() {
        return n;
    }
     static void print() {
         cout << n << " " << m << endl;

     }
private:

    //累计创立了多少个对象
    static int n ;
    //正在使用有多少个对象
    static int m ;
    //缺省值是初始化列表用的,所以不能给缺省值
};
静态成员函数不能访问非静态成员变量
int A::n = 0;
int A::m = 0;//声明和定义分离

-----------------------------------------------------------------

int main() {
    A aa1;
    A aa2;
    当n和m为共有时,可以直接通过类名访问
   法一: cout << A::n << " " << A::m << endl;//2 2
   法二: cout << aa1.n << " " << aa2.m << endl;//2 2
   法三: A* ptr = nullptr;
    cout << ptr->n << " " << ptr->m << endl;//2 2

  当n和m为私有时,只能通过静态成员函数来访问

    A();
    A::print();
  
}

  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值