c++类和对象细节汇总


类和对象细节总结

struct

c++兼容c语言,保留了c语言中struct的用法,同时把c语言中的结构体含义上升为类。

//c语言用法
typedef struct ListNode*
{
    struct ListNode* next;
    int val;
}
//c++用法
typedef struct ListNode*
{
    ListNode* next;
    int val;
}

c++也可以继续使用c语言的用法。c++将struct的含义上升以后实例化对象就不需要在typedef和写struct了。

struct与class:

  • struct默认所有成员变量和成员函数是public的,而class默认是private.
  • 除此之外,class和struct没有区别,使用struct定义类也有this指针的机制,也有const,static的用法。

成员函数的inline

如果成员函数在类内定义,那么只要符合条件,就会被设置为内联。即在类内定义的成员函数是默认都加了inline的,尽管它们最终可能不会当成内联处理,但是它们加了inline,一定不会进符号表

如果成员函数的声明和定义分离,那么就是普通函数,会进符号表。类里面的成员函数只要声明和定义分离就是普通函数,不分离就会被编译器默认加inline.

推荐类里面短小的,频繁调用的函数放在类内作为内联,而大函数声明和定义分离。声明和定义分离的意义是通过看.h就能看到整个类的情况,方便读代码。

类域

class在定义类的同时也产生了类域,类域影响对内中成员的访问,要想访问类中的成员,需要指定在类域中查找。

类域和命名空间域会影响访问,决定对象生命周期的因素是对象存在进程地址空间的哪一个区域,对象是在哪个区域定义的。区分一个变量的声明和定义要看是否开空间,没有开空间的话就是声明,开了空间就是定义。因此在.h文件中要谨慎定义全局变量,否则多个.cpp包含的时候就容易出现重定义的错误。

注意全局的静态变量生命周期是整个程序的生命周期,但是全局的static变量作用域只有当前文件,而普通的全局变量作用域是整个项目。对于局部的静态变量,生命周期还是整个程序的生命周期,但是作用域只有它所在的函数体内。

类的存储方案

  1. **(错误方案,不使用)**把成员变量和成员函数都算作类的大小,每实例化一个对象就给它的变量和函数开空间。这种太浪费空间,而且多余,没有必要

  2. 将成员变量算入类的大小,同时存一个指针,指针指向一块专门存放成员函数的类成员函数表。每当对象需要调用成员函数时,可以通过指针找到那张表,在表里面找到要调用的函数。在虚表多态中采用这种方案,但是普通类的存储方案不是这个。

  3. 普通类的存储方案是将成员变量算入类的大小,将成员函数放在公共代码区,形成一个类成员函数表,哪一个对象要调用成员函数,就在这个类的公共代码区找,找到了以后把自己的地址传入成员函数,成员函数用this指针接受。这样不仅成员函数放在公共代码区,简化了类的设计,减小了对象的大小,节省空间,而且还能区分是哪个对象调用的成员函数。这种方案是在进行编译的时候就到这个类的公共代码区中去找要调用的成员函数,不是在运行的时候去找。

    class A
    {
    public:
        Func(){}
    };
    A* ptr=nullptr;
    ptr->Func();
    

    上面代码可以正常运行,因为虽然this=nullptr,但是在成员函数里面没有调用this指针,而且这里在进行函数访问的时候根本就没有进行解引用操作,虽然写法是ptr->Func(),看起来是调用ptr指向的对象的Func函数,实际上是在公共代码区去找该函数,该函数不属于ptr指向的对象,属于整个类,在编译阶段就已经在call Func函数了,不是在运行的时候在解引用去找。

  4. 类采用第三种存储方案,可以把成员函数理解为全局普通函数,但是是属于这个类的普通函数。对象调用函数就是在调用这些普通函数,只不过把自己的地址传了进去。这些普通函数只不过有inline和非inline之分而已。

  5. 在计算类的大小时,只算成员变量,不算成员函数,而且和c语言的结构体一样要考虑内存对齐。注意空类和只有成员函数的类的大小是1,作为占位标志,不存储实际数据,以标识对象存在。

this指针

每一个类中的非静态成员函数都有this指针,用来接收调用对象的地址,以日期类为例,this指针的原型是Date* const this,即this指针的指向不能改变,a调用成员函数,a就传入它的地址,this指针接收,this指针就只指向a,不能改变指向。但是this指针指向的内容可以改变,如果想要this指针指向的对象的内容不能被修改,可以在成员函数的后面加const,这个const修饰*this

class A
{
public:
    void Func()const
    {
        cout<<this<<endl;
        cout<<_a<<endl;
    }
private:
    int _a;
}
A* p=nullptr;
p->Func();错误代码,调用实际上是this->_a

上面代码如果不对this指针进行this->操作,就能正常通过。

this指针是成员函数隐藏的第一个形参,this指针存在的区域是栈上,有时为了提高效率,可能被优化到寄存器。

当定义一个A类型的对象a时,调用Func函数,实际上是a.Func(&a).只不过不用显示的传地址和写this指针。成员函数调用的精华就是this指针。

类的成员函数

一个类中,自己不显示写构造函数,编译器会默认生成一个构造函数,默认生成的构造函数对于内置类型不做处理,对于自定义类型会去调用它的默认构造。根本原因是在过初始化列表的时候没有进行自己初始化,于是只能走默认的初始化。

系统生成的默认构造函数就相当于这样:

class Mystack
{
public:
    Mystack():q1(),q2(){}
private:
    Queue q1;
    Queue q2;
    int size;
};

所以才会出现对于自定义类型不处理,对于内置类型调用它的默认构造。

使用类进行实例化对象需要注意的细节:如果调用默认构造函数,直接Date d,不要加括号Date d().会出现二义性,Date d()可以被认为是函数的声明,表意不明确。

默认构造函数有三种:指的是不传参也能用的构造函数。系统提供的默认构造,自己写的无参的默认构造,还有就是全缺省的默认构造。注意默认构造函数只能有一个,否则会出现调用不明确的情况。

拷贝构造函数Date(const Date& d)和赋值运算符重载Date& operator=(const Date& d)也是类的默认的成员函数,拷贝构造函数也有初始化列表,只要是构造函数就有初始化列表,只有构造函数才有初始化列表。初始化列表默认调用自定义类型的默认构造函数,对于内置类型不处理,只是内置类型定义的地方。

编译器默认提供的构造函数对于内置类型不做处理,c++11打了一个补丁,是给初始化列表用的。

class Date
{
private:
    int _a=1;//打了补丁依然还是声明
};

构造函数的任务是对对象进行初始化,析构函数的任务是对象中资源的清理。对象的创建和销毁工作是操作系统做的,创建对象和销毁对象开辟栈帧和销毁栈帧,如果在堆区,就是new和delete

默认系统提供的析构函数大部分情况下就够用,默认提供的析构函数对内置类型不处理,对自定义类型调用它的析构,一般只要对象在堆区开辟了空间,才要自己写析构。

拷贝构造函数的参数是引用,拷贝构造函数也是构造函数,存在初始化列表,拷贝构造函数对于内置类型完成逐字节的浅拷贝,对于自定义类型调用它的拷贝构造。系统默认的拷贝构造函数类似这样:

class Mystack
{
public:
    MyStack(const Mystack& my):q1(my.q1),q2(my.q2),size(my.size){}
private:
    Queue q1;
    Queue q2;
    int size;
};

拷贝构造函数参数要加const,且不能传值,否则会引发无穷递归。拷贝构造一个对象的写法只有2种:Date d2(d1);Date d3=d1,第二个不是赋值运算符的重载,而是拷贝构造。要注意与string a="hello"区分,后者=左右类型不同,发生了隐式类型转化,是构造+拷贝构造被优化为直接构造。前者=左右两边的类型相同,是直接调用拷贝构造。

使用指针也能完成拷贝构造函数的任务,但是这个不是拷贝构造,就是一个普通的构造函数。

class Date
{
public:
    Date(const Date* p):_a(p->_a){}
private:
    int _a;
};
Date d2(&d1);//这里是调用取地址的构造函数

可以Date d2=&d1;,这里发生了隐式类型转化,&d1是指针,d2是Date类对象。先调用构造函数 Date(const Date* p):_a(p->_a){}构造一个tmp,在调用拷贝构造函数构造d2,优化为Date d2(&d1).写了Date(const Date* p):_a(p->_a){}依然会有默认的拷贝构造函数,如果自己写了拷贝构造函数就不会有编译器默认生成的拷贝构造函数。

class Mystack
{
public:
    MyStack(){}
    MyStack(const Mystack& my)
    {
        //深拷贝操作
    }
private:
    Queue q1;
    Queue q2;
    int* _str;
};

自己写的深拷贝在过初始化列表的时候也会默认对于自定义类型调用它的拷贝构造函数。上面自己写的深拷贝相当于:

MyStack(const Mystack& my)
{
    //深拷贝操作
}
等价于:
MyStack(const Mystack& my):q1(my.q1),q2(my.q2),_str(my._str)
{
	//深拷贝操作
}

注意:如果自己只写拷贝构造函数,不写默认构造函数,系统不会默认生成默认构造函数,会出现没有可用的默认构造函数的情况。因此,如果自己显示写了拷贝构造函数,就要提供默认构造函数或者别的构造函数。若自己写的是空实现的拷贝构造函数,则该拷贝构造函数的作用与自己不写系统默认提供的是一样的。

运算符重载

运算符重载原型:

Date d1,d2;
// ostream& operator<<(ostream& cout,const Date& d);
cout<<d1; // operator(cout,d1)
cout<<(d1==d2)<<endl; // d1.operator==(&d1,d2),地址是this接收

类内部进行运算符重载的时候左操作数是this.

构造与析构顺序

在不同区域的对象构造和析构的问题:

class A{};
A a;
int main()
{
    static A b;
    A c;
    A d;
    static A e;
    return 0;
}

全局对象先构造,如果有多个全局对象,按照顺序依次;在函数里面的对象后构造,按照顺序依次构造。在栈上的对象先销毁,在栈顶的先销毁,栈底的后销毁;然后是静态区的在销毁,也是后构造的先销毁。栈的使用是先用高地址,再用低地址,在函数栈帧内部也是一样;其他区域是先用低地址,再用高地址。堆栈是相对而生的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r9EhuACk-1662186380637)(C:\Users\19199\AppData\Roaming\Typora\typora-user-images\image-20220902164117910.png)]

拷贝构造函数场景

以值的方式返回对象是返回的对象的拷贝,会调用拷贝构造函数,而以引用的方式返回则不会。

A Func()
{
    static A a;
    return a;//会有拷贝
}
int main()
{
    Func();//编译器发现没有使用返回的拷贝对象,会立刻调用它的析构函数销毁它。
    return 0;
}

赋值运算符重载是用一个已有的对象给另一个已有的对象赋值,赋值运算符重载很像拷贝构造函数,但是与拷贝构造函数有本质区别,虽然它们都是类的默认成员函数。但是拷贝构造函数是构造函数,有初始化列表,而赋值运算符重载是运算符重载不是构造函数,也没有初始化列表。

连续赋值行为默认是从右向左进行的。

int i=0,j=1,k=2;
i=j=k;// j=k,返回j;i=j,返回i
Date d1;
d2=d3=d1;// d3=d1,返回d3;d2=d3,返回d2

赋值运算符重载不能重载成全局的,否则在调用的地方会出现调用不明确,类外的和类里面的都可以调用,冲突了。如果在类外写了赋值运算符重载,类里面也会生成默认的,就会产生调用冲突。如在类里面手写了赋值运算符重载,就不会产生默认生成的运算符重载。默认的赋值运算符重载对于内置类型进行逐字节的值拷贝,对于自定义类型会调用它的赋值运算符重载。

若自己手写了赋值运算符重载,系统就不会提供默认的赋值运算符重载,而且自己手写的赋值运算符重载不能继承默认提供的运算符重载的功能:对于内置类型逐字节拷贝,自定义类型调用它的赋值运算符重载。这些功能都要再去自己实现。如果对象在堆区没有开辟空间,就直接使用系统提供的默认的就好。

赋值运算符重载与拷贝构造:

Date a,c;
Date b=a;//直接拷贝构造
a=c;//调用赋值运算符重载
string f="hello";//构造+拷贝构造->直接构造

前置++与后置++

前置++返回++以后的结果,后置++返回++以前的结果

Date& operator++();//前置++
Date operator++(int);//后置++
++d1;//d1.operator++(&d1);
d1++;//d1.operator++(&d1,0)

一般operator-()可以复用operator-=,operator+()可以复用operator+=(),-=改变自己,-不改变自己。+=改变自己,+不改变自己。

一般成员函数都有this指针,在类里面,一个成员函数可以调用另一个成员函数。例如:

class A
{
public:
    int Func1()
    {
        return 0;
    }
    void Func2()
    {
        Func1;
    }
};
//实际过程如下
int Func1(A* const this)
{
    return 0;
}
void Func2(A* const this)
{
    this->Func1(this);
}

类里面调用普通成员函数也是要默认加this的。

运算符重载与函数重载的区别:

  • 运算符重载是让自定义类型的对象可以使用运算符。如果不进行运算符重载,自定义类型的对象是不能使用运算符的,包括&,自定义类型的对象可以&就是因为类的6个默认成员函数中有2个是&运算符的重载。
  • 运算符重载让自定义类型的对象可以使用运算符,转化为调用对应的函数
  • 函数重载是允许有同名的函数,增加函数名的复用性
  • 运算符重载也可以构成函数重载。

<<和>>的重载

流插入和流提取的重载只能写在类外,因为写在类里面this指针占据了第一个位置,使用起来不合常规。写在类外又想访问类里面的私有成员,就要使用友元的技术,友元可以放在类的任何地方,一般放在类的最开始。

istream对象,ostream对象的2个特点:

  1. 没有默认构造函数。
  2. 拷贝构造函数是private的,所以不能传值或以值的方式返回。

运算符重载的注意事项:

  • 不能创建新的操作符,如operator$()
  • 重载的操作符必须有一个类类型的参数,参数不能全部是内置类型,必须要有一个是类类型,因为运算符重载本来就是给自定义类型使用的,让类类型也能使用运算符。
  • 运算符重载不能改变运算符原来的含义,不能乱重载
  • 运算符重载作为成员函数重载的时候,第一个参数的位置被this指针占据
  • 这些运算符不能重载:.*(点星) ::(域作用限定符) sizeof ?:(三目) .(点操作符),注意*(星,解引用)是可以重载的

const

const成员变量不能直接修改,并且一定且只能在初始化列表初始化,并且一定要初始化。

const成员函数的const放在函数后面,实际上修饰*this,即成员函数后面加了const的话,在该成员函数内部不能修改调用该函数的对象。

void Date::Print(Date* const this);//普通成员函数
void Date::Print(const Date* const this)const ;//加了const的成员函数

任何一个对象在调用普通成员函数的时候都会把自己的地址传进去,const本质就是在修饰这个地址。

注意const的成员函数和非const的成员函数是构成函数重载的,在对象进行调用的时候,优先调用最匹配的,虽然普通对象也能调用const的成员函数。

取地址重载

类的6个默认成员函数中,有2个是取地址重载。由于类类型是不能直接使用运算符的,所以要进行重载,取地址也是一样。默认的取地址重载:

class Date
{
public:
    A* operator&()
        return this;
    const A* operator&() const
        return this;
}

如果不想让人拿到地址,可以这样做:

A* operator&()
     return nullptr;
const A* operator&() const
    return nullptr;

也可以把&重载设置为私有,这样就没有可用的&重载,也就拿不到地址了。

初始化列表

初始化列表是成员变量定义的地方,有些成员必须在初始化列表初始化。

  • 引用变量成员,引用必须在定义的时候初始化,而初始化列表就是定义的地方
  • const成员变量,const成员变量只有一次初始化的机会,一旦初始化,后面就不能直接修改。c++语法规定const成员变量必须在初始化列表进行初始化,不初始化的话就无法编译通过
  • 没有默认构造函数的自定义类型成员必须在初始化列表初始化,否则编译报错。

区分三个概念:成员变量声明,对象的定义,成员变量的定义。

成员变量的声明是在类里面,成员变量就已经声明;对象的定义是用类实例化对象;成员变量的定义是在对象定义的时候自动调用构造函数,然后过一遍初始化列表的时候定义的。

class A
{
public:
    A(float c):_c(c){} //这里不要在写成A(float c):_a(),_b(),_c(c),多此一举,而且还没有用到缺省值,是随机值
private:
    int _a=1;//这里的缺省值是给到初始化列表的
    const _b=2;
    float& _c;//这些是声明
};
A a;//对象定义

如果在初始化列表中要想用到c++11补丁的缺省值,就不要在初始化列表显示写那些有缺省值的成员变量了。

初始化列表中定义的顺序是按照成员变量声明的顺序来的

注意下面的例子是可以通过的:a是随机值,b是3。const成员变量要在初始化列表初始化,它初始化的顺序也是要按照声明的顺序来的。

class A
{
public:
   A(int _b):b(_b),a(b){}
private:
    int a;
    const int b;
};
int main()
{
    A a(3);
	return 0;
}

explicit

explicit可以禁止隐式类型转换。

class Date
{
public:
	explicit Date(int a):_a(a){}
private:
	int _a;
};
Date d = 1;//隐式类型转换,先构造,在拷贝构造->直接构造

在构造函数的函数名前面加explicit可以禁止隐式类型转换,这样就不能像第8行那样构造.只要类型不同且使用=或者类型不同发生拷贝构造就会有隐式类型转换。例如string传参。

void Func(const string& a)
{
	cout << a;
}
int main()
{
	Func("abcdef");
	return 0;
}

上面的参数使用string& a就不行,因为发生隐式类型转换要生成临时的string tmp,tmp具有常性,必须用const string& 接收。

匿名对象

匿名对象的生命周期只有一行,过了这一行,立刻调用析构函数。

cout<<string("hello")<<endl;

static

题目:要求统计一个A类型的对象创建了多少个

class A
{
public:
	A() { count++; }
    static int Num_Of_A()
    {
        return count;
    }
private:
    static int count;
};
int A::count = 0;

static的成员变量必须在类内声明,在全局进行初始化,初始化的时候不要带上static。static的成员变量不能给缺省值,因为缺省值是在初始化列表发挥作用的,static的成员变量是在类外初始化的,所以不能给缺省值。static成员变量必须在类外全局初始化了才能用,如果不初始化的话使用会报错,极端情况是既不在类外初始化也不使用。static的成员变量属于整个类,不属于某一个对象,也要受到访问限定符的限制,该变量放在静态区。static的成员变量不计入类的大小

静态成员变量属于整个类,生命周期在全局,存放在静态区。

void Func()
{
    cout << this->count << endl;
}
A* p=nullptr;
p->Func();

注意这里访问静态成员变量count并不是通过this访问,只是写法这样写。实际上是通过整个类在访问。

静态成员函数:静态成员函数没有this指针,属于整个类,由于静态成员函数没有this指针,也就不能访问普通成员函数和普通成员变量,因为在成员函数内部访问另外一个普通成员函数或普通成员变量都是通过this->访问的,只是一般不写this而已,但是编译器会加上。而静态成员函数没有this,自然也就不能访问普通成员变量和普通成员函数。

题目:设计一个类,只能在栈上实例化对象。

class Stack_only
{
public:
    static Stack_only Make()
    {
        Stack_only so;
        return s0;
    }
private:
    Stack_only(int x=0,int y=0):_x(x),_y(y){}
    int _x;
    int _y;
};
int main()
{
    Stack_only s=Stack_only::Make();
    return 0;
}

可以使用类名调用Make函数,调用函数由于默认是inline,直接展开,相当于在main函数栈帧里面创建了对象。同理可以设计一个只能堆上实例化对象的类,只能在静态区实例化对象的类。

static Stack_only* Make()
{
    return new Stack_only;
}
static Stack_only* Make()
{
    static Stack_only s;
    return &s;
}

这种设计模式类似于以后要学的单例设计模式。

静态对象,静态成员函数,静态成员变量的比较:

  • 静态对象除了生命周期在全局以外,其他的和普通的在栈上开辟的对象没有差别
  • 静态成员函数没有this指针,也是属于整个类的,静态成员函数只能访问静态成员变量,静态成员函数可以用类名访问,普通成员函数不行。
  • 静态成员变量是属于整个类的,静态成员变量的生命周期是全局,静态成员变量不计入类的大小,不能给缺省值,在类内声明,类外初始化
  • 静态成员函数没有this指针,不能加const

友元

  • 友元分为友元函数和友元类,友元函数可以访问类的私有和保护成员,但是注意友元函数不是类的成员函数,如果友元函数是全局函数,则没有this指针,而且不能用const修饰,因为只有成员函数(非静态)才有this指针,才能用const.静态成员函数是不能用const的,因为没有this指针。
  • 友元函数可以在类的任何地方声明,不受访问限定符的限制
  • 一个函数可以是多个类的友元函数,友元函数除了可以访问类的private和public以外,其他的和普通函数没有区别

内部类

类作为友元具有单向性,不可传递,不可继承。

class A
{
private:
    int _h;
public:
    class B
    {
	public:
        void Func()
        {
            cout<<_h<<endl;
        }
	private:
        int _b;
    }
};

B可以访问A的私有,A不能访问B的私有,内部类B除了是A的友元,收到A的访问限定符的限制,其他性质和全局的类一样。注意内部类B不计算入A的大小,sizeof(A)=4.

如果有2个全局的类,声明一个类作为另外一个类的友元可以在类里面写friend class A,可以放在另外一个类的任意位置。

友元会增加耦合度,设计一个程序,低耦合度才是目标。所以尽量减少友元的使用。

连续构造会优化

连续一个表达式步骤中,连续构造一般会优化。即构造+拷贝构造->优化为直接构造

class A{};
void Func(A a){}
Func(A());

匿名对象构造+传值a的拷贝构造->直接拷贝构造

A Func()
{
	A ret;
    return ret;
}
int main()
{
    A a=Func();
}

返回ret要调用构造,a又拷贝构造,优化为直接构造。一般在计算拷贝构造函数的调用次数时,需要考虑这样的优化。一般拷贝构造+构造也会优化为直接构造,例如上面的代码,上面的代码编译器会在Func函数return之前就直接用ret构造a,即Func函数的栈帧还没销毁就已经把A构造出来了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值