C++基础知识-Day5

今天主要讲的是类的扩展

1.类成员函数的存储方式

首先我们介绍类成员函数的存储方式,C++引入面向对象的概念之后,C语言中的一些比如static/const等原有语义,作一些升级,此时既要保持兼容,还要保持冲突不变。一个对象所占的空间大小只取决于该对象中数据成员所占的空间,而与成员函数无关,但是对于对于一个类来说,加入其有十个对象,那么其类成员的存储方式有两种方式:每个对象具有一个函数成员、多个对象共享同一个公有函数,下面我们分别对其进行分析

第一种情况:

假设一个类定义了十个对象,那么就需要分别对10个对象和函数代码分配存储单元

理论上说,每个对象有自己的数据和函数段是可以的,但是这样会耗费大量的空间,因此我们思考能否只用一段空间来存放这个共同的函数代码段,在调用对象的函数时,都去调用这个公用的函数代码

显然,这样做会大大节约存储空间

那么就会出现疑问:当对象调用公用函数时,如何确定访问的成员是调用对象的成员呢?-利用this指针

C++设置了this指针,对象在调用公用函数时,并将对象的指针作为隐含参数传入其内,因此在对象1调用公用代码时,将对象1的地址传入公用代码

类的成员函数只有一个,在对象调用过程中,传入对象的地址,具体可以表现为this

3. const在类中的扩展(无论const修饰什么,都是不能修改的)

(1)  const修饰数据成员,成为数据成员,可能被普通成员函数和常成员函数来使用,不可更改

(2)  必须初始化,可以在类中(不推荐),或初始化参数列表中(这是在类对象生成之前唯一一次改变const成员的值的机会了)

(3)  初始化列表,一方面提高了效率,另一方面解决一类因面向对象引入的一些新功能的特殊安排

4. const应用

(1)  见下面代码

这样直接进行编译会报错,报错的原因是const修饰的x没有初始化,使用的是系统默认的初始化,其肯定不会对x进行初始化的,但是如果直接将const去掉即不会报错,因此通过对比我们可以知道,const修饰类数据成员时,必须要初始化         

那么,既然const必须要进行初始化,那么初始化const的方式又有哪些呢

一种情况:直接在类内部进行初始化直接const int x=100;

  另一种情况:初始化列表initial list,因此有const的数据成员常需要构造器

使用初始化列表的原因,一方面是效率的问题,另一方面是为了一些新扩展的功能提供一个解决场所或者办法

(3)如果含有引用的话

相当于中间产生了一个临时变量zz,

(4)  也可以使用传参的方式,但是只能够对其进行一次的修改

const修饰的数据成员,可以在非const 函数中使用,但是不可以更改

(5) const修饰类函数成员,不可以修饰全局函数

const有几个放置的位置

const void foo(), void const foo()->这两种方式都是修饰返回值的

void foo(const int x)->这种方式是修饰参数的

void foo() const->修饰函数

(6)   const构成的重载问题->其修饰函数时可以构成重载,重载函数是根据语境来确定哪个函数被调用,const构成的重载

非const对象:优先调用非const版本,在没有非const版本的时候,也可以调用const版本

const对象,只能调用const版本,很多库常见提供两个版本

 

如上,首先调用的是非const版本

(7)  const修饰函数以后,承诺不改变,在本函数不会发生,改变数据成员的行为,只能调用const成员函数

在const修饰的函数中发生了改变数据成员的行为,因此是不可以的

(8)  inline  const  static,声明关键字或者说是定义关键字

(9)  const小结

  1. const修饰函数,在声明之后,实现体之前
  2. const函数只能调用const函数,非const函数可以调用const函数
  3. 如果const构成函数重载,const对象只能调用const函数,非const对象优先调用非const函数
  4. 类体外定义的const成员函数,在定义和声明处都需要修饰const修饰符,有些关键字是定义型的,有些关键子是声明

(10)  const修饰对象:

const修饰的对象,其内可以有非const数据成员,但不可修改,只能调用const成员函数

针对const有可能修饰对象,往往提供两个版本,构成重载

5. static在类中的扩展

static可以修饰局部和全局,修饰局部变量,扩展其生命周期和存储位置

修饰全部变量,本身全局变量有外延性,加了static就只能仅限于本文件使用

C++扩展了static在类中的语义,用于实现在一个类,多个对象中共享数据,协调行为的目的。

静态变量有全局变量的优势,又不会像全局变量一样被滥用,而用于管理静态变量,就需要用到静态函数

类的静态成员,属于类,也属于对象,但终归属于类

(1)  static修饰数据成员

  1. static修饰数据成员,需要初始化才能使用,不可以类内初始化,必须类外初始化,需要类名空间,且不需static
  2. 类的声明与实现分开的时候,初始化在.h还是在.cpp中?.cpp中,在实际书写过程中我们应该将其初始化写在.cpp中,类的声明之前
  3. static的大小,添加static之后会不会占用类的大小呢?static声明的数据成员,不占用类对象的大小,其存储在data段的rw段_m, _n, _share是一个整体命名空间即类名是维系这个整体的基础
  4. 访问。 _m,_n 依赖于对象,对象生成了才可以访问  _share 不依赖与对象,在对象生成之前就已经存在了
  5. 通过以上代码段我们可以得出,

    static既可以通过对象访问,也可以不通过对象,直接通过类名访问

  6. static修饰数据成员总结 
  1. 共享:static成员变量实现了同族类对象间信息共享
  2. 初始化:static成员使用时必须初始化,且只能类外初始化,声明与实现分离时,只能初始化在实现部分(.cpp)
  3. 类大小:static成员类外存储,求类大小,并不包含在内
  4. 存储:static成员是命名空间属于类的全局变量,存储在修饰函数成员data区的rw区
  5. 访问:可以通过类名访问(无对象生成时亦可),也可以通过对象访问

(2)static修饰数据成员

以下几个演变过程

但是这样是很依赖于对象的,如果将cout<<A::fooCount<<endl写在大括号外面,则是不成立的,为了不依赖于对象,我们可以加一个函数访问其私有成员

为了使函数不依赖于对象,可以将invokefooCount函数设置为static,这样就可以直接将m. invokefooCount()改成A:: invokefooCount(),这样就可以实现函数不依赖于对象

static修饰函数,目的是为了管理静态变量

(3)  static应用:一塔湖图,共享图书馆内的书籍

首先我们将需要共享的变量_lib写成公有变量,利用static关键字使其成为共享的变量

但是对于数据成员_lib一直暴露在外面使得整个函数的封装性不是很好,为了解决这个问题,我们将_lib写成私有成员变量,并且利用&getLib()函数来访问_lib

(4)  由上可以看见,static修饰成员函数,主要用来管理静态变量,类内定义需要加static ,类外定义不需要加static

(5)  静态成员函数只能访问静态的成员(数据成员和函数成员),不能访问非静态的成员

这种情况是会报错的,是因为静态函数只能访问静态数据成员而不能访问非数据成员,

发生这种情况的原因:对于静态函数,有两种情况,可以直接通过对象去访问,也可以通过类去访问,但是通过类去访问的话由于没有类,因此就不会有this指针,但是通过对象去访问的话是有this指针的,这样就会存在矛盾,所以static函数是没有this指针的,但是普通函数是有this指针的,因此static函数是不可以访问非static函数的

(6)  非静态函数是可以访问静态成员的

(7)  static函数的应用:取号服务

 

(8)  静态函数小结

a. 静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装

b. static修改成员函数,仅出现在声明处

c. 静态成员函数只能访问静态数据成员,原因:非静态成员函数,在调用时this指针被当作参数传递,而静态成员函数属于类,而不属于对象,没有this指针

(9)  课堂实战:单例模式:一个类仅有一个实例的形式,实现共享用的

为了实现单例模式,我们首先将构造器私有化,此时不能通过常规的手段生成对象,因此拷贝构造器也是私有化,防止生成一个单例之后利用拷贝构造器生成新的对象

最终打印出来ps和ps2的地址是一样的,因此我们可以得出,使用单例模式只能得到相同的地址

在内存管理里面,有下面几种情况

见new见delete

见new不见delete

不见new见delete

不见new 不见delete

(10)  课堂实战:Render Tree渲染树

一套成熟的类库,通常都会引入内存管理,从使用的角度来说,只见new不见delete,或是自始至终见不到new和delete

比如说cocos中渲染树,就是一种内存管理手段,对象只管生成,参与渲染和销毁由渲染树来管理,今日实战的目的:每生成一个对象,将其挂在一个链表上,最终我们能够访问这个链表

要实现这个功能,所有生成的对象肯定是共享表头的,整个渲染树的步骤为:对象的创建,初始化,挂到树上去

autoRelease的思想,在所有对象都没有存在的时候,就已经存在了head,首先生成了节点A,那么head->A,随后生成了节点B,那么新生成的节点仍然满足:让新来的节点有所指向,那么首先是节点B指向节点A,然后是head的next指向节点B,所以说还需要一个next,创建对象A之后,有一个this指针指向A

那么这一段代码一共有两种情况,一种情况是当创建一个对象之后,发现head是空的,那么只需要将head指向A的this指针,并且将A的next置空

另一种情况是当创建一个对象之后,head是不为空的,那么需要使新来的节点有所指向,此时this是指向的对象B,this->next=head;head=this

但是我们细心的可以发现,如果将if内部的语句调换一下顺序,那么if和else内部的语句格式都是一样的,问题的关键就在于head,当head=nullptr的时候,就将this->next置空,那么直接将this=head;优化后的版本如下

最终渲染树的代码为

(12)  static const初始化

static const int a; 其中static和const都做a的定语,其中stati更重要,其存在于data段的ro段,因此整个初始化的方法为static const int a=100;

6. 指向类成员的指针

C++扩展了指针在类中的使用,使其可以指向类成员(数据成员和函数成员),这种行为是类层面的,而不是对象层面的

(1)  C语言中的指针

(2)  C++中的指针

定义一个指针,指向类的成员,不是指向对象的成员

下面讲的指针,是指类层面的指针,而不是对象层面的

在C++中,为了实现指向类的指针,我们常采用如下方法

(3)  Pointer to func member

定义一个指向非静态成员函数的指针必须在三个方面与指向的成员函数保持一致:参数列表要相同,返回类型要相同、所属类型要相同

由于类不是运行时存在的对象,因此,在使用这类指针时,需要首先指定类的一个对象,然后,通过对象来引用指针所指向的成员

在(s.*pdis)(1)中,刚开始写成的是s.pdis(1),但是由于.*和->*的级别都不是很高,因此加个括号才是正确的

Pointer应用:提供更加隐蔽的借口

7. Pointer本质:

假设上述是一个类,该类中有a,b,c三个私有成员变量,分别占用的字节数为:1,4,8,则a的起始地址为0,b的起始地址为1,c的起始地址为5,假设有一个对象,其访问形式为:cout<<obj.*pointer<<endl;对象本身是有一个地址的,若指针指向的是a,则在obj的地址的基础上加0,若指针指向的是b,则在obj的地址的基础上加1,以此类推

posted @ 2018-12-16 21:20  Cucucu  阅读(131)  评论(0编辑  收藏  举报