从OOP的角度重看C++(一)——背景与基础知识

从OOP的角度重看C++(一)——背景与基础知识

    记得本科的时候初次接触C++的时候,没有好感,不喜欢老师的讲课风格,导致C++一塌糊涂,没有什么概念,除了完成老师作业的时候,用的和C几乎没有差别,真是不懂C++有什么用处。

    现在,上研了,除了做科研,没有接触过工程,但是,偶尔的一点代码都会让自己觉得恶心,混乱的编程风格、不知所以的定义类,不明确的方法定义,都让我觉得有必要学习一下面向对象的思想。这学期决定蹭陈老爷子的课,听听他老人家的思想。

    第一堂课没有去,大概就是讲了个为什么需要学习面向对象。因为硬件和基础设施的发展、因为行业对应用软件的更高要求等等,程序员们要想写出更好的程序就不得不需要自定义的数据类型。在Pascal之前的程序设计语言,是封闭的,即用户只能使用语言的基本类型,可是程序员总是希望他们能够在基本类型以外,定义自己所需要的新的类型,这就要求程序设计语言的类型系统是开放的——可以使用类型构造符构造自己需要的新类型。这就引入了以下的一些概念:

################################第二次课#############################################

类型:包含了值集(枚举法、描述法,限定法描述)和操作集(语法、语义)

系统 :一组构件,为了完成一个共同的目标而协同工作。

类型系统 :一组相互关联的类型,它们的实例协同工作,实现一个共同的目标。包括封闭系统和开放系统,如果一个系统不容许使用自定义类型,那么这个系统就是封闭的。

    一个程序设计语言就是一种类型系统,如果他允许用户自己定义新的类型,那么他就是开放的(现在的程序设计语言基本都满足开放性);如果不支持数据抽象,那么他就的操作集就不能封装值集,就是不安全的。

    所以,通常需一个安全的机制——类型化:用类型系统来检查、管理和控制对应的程序。
    类型化是程序设计语言的一种内在属性,是支持应用程序中的类型和类型系统的。没有这种支持(例如没有类型定义机制和类型检查机制),应用程序中就不可能有类型。他有两个主要作用:抽象作用和限制作用(防止基本逻辑单元之间交互的非法性)。

    从类型化的特征来看,现在的程序设计语言主要可以分为两类

    静态类型化语言——在程序编译的时候就进行类型检查;比如在C++中,声明一个数组,必须提前给出数组的长度,因为编译器要提前分配内存,这也是很多人 觉得C或者C++不好用的原因(相反的,在运行时才检查的就是动态类型语言。在用动态语言编程时,不用给变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来。)

    强类型化语言——表达式必须是类型一致的,也就是说没有强制类型转化前,不允许两种不同类型的变量相互操作。强类型定义语言是类型安全的语言。(弱类型语言则是允许将一块内存看成多种类型的语言,比如C++允许将int 和char类型做加法等

       所以说,静态类型化语言和强类型化语言之间没有必然的联系,不要把他们弄混了。因此在实际的项目中,可能会根据项目的具体要求而选择不同的语言。


################################第三次课#############################################

        第二节课,主要讲了类型系统的两个本质特质:第一个是数据抽象(核心),第二个是多态(4种)。老师引入这两个概念的时候举得例子也很好,通过程序设计语言本身的类型和用户自定义的数据结构在操作时候的4种差异来说明这个问题的(前提是这个语言不允许数据抽象,,,即面向对象之前的那些语言)。

1.封装与未封装性:任何一个程序设计语言都对他的基本类型是封装的,即我们在使用操作符的时候只能将基本类型当做整体,不可能使用它们的某个分量。而我们自己定义的数据结构则是可以随意的操作里面的分量,因为我们自己定义的数据结构都是基于基本类型的,现有的操作也是基于基本类型的,也就是说操作没有封装值集,这样就存在着一种安全隐患——某个操作在逻辑上非法,但是编译的时候无法检测出来。

2.操作集与值集的对应。这个其实是由于封装性造成的,因为我们无法对我们的数据集封装,所以操作集其实是混乱的,即只要对一个可以使用该操作的类型,那么编译的时候都不会报错,这么多么不好啊!

3.区分类型的设计者和使用者。设计者是清楚的知道一个类型到底包含了啥,可是使用者却不用知道,只需要知道他到底怎么用(那些操作)就可以了。这点在程序设计语言中也体现的很好,我们在使用基本类型的时候,知道他们有那些操作,直接用就可以了,而且系统也不用担心我们的操作会改变这些基本类型,因为我们根本就没有设计者的那些权限啊~但是对于编程人员自己写的数据结构来说,则不一样是这样,,,,

4.自定义类型中的操作不能使用基本类型中的操作已经占用的标识(包括操作符和函数名),而只能通过定义新的函数或过程来实现作已经占用的标识(包括操作符和函数名),而只能通过定义新的函数或过程来实现,也就是说,我们无法使用常用的名字或者符号作为我们自己定义的数据结构的标识,因为他们已经属于基本类型了!感觉好不平等的说委屈

        个人觉得以上4点其实正是说明了两个问题:前三点说明了 数据抽象的必要性,最后一个则是说明了 多态的必要性。

        在软件越写越大之后,问题也会很多,程序员们都想自己写的系统可以像程序设计语言那么规范,好控制,因此,希望语言提供数据抽象的需求就变得更强烈。B. Meyer说过一个软件工程师应当掌握的13个基本原理里,第一个就是 抽象——Abstraction。因为他是应对复杂性的关键,能够简化分类。

        在程序设计语言中,抽象包含两类:功能抽象数据抽象

        功能抽象是通过过程(某些语言将没有返回值的函数叫做过程)或者函数的声明来完成的。即,在程序中提供了一种只说明这个过程或者函数具有某种功能,但不提供细节的手段。比如,C++中的extern关键字,如果他只是作为声明,那么系统是不会分配内存的,只是说明这个变量的定义在其他地方,此处只是声明了一下。(当然,如果是作为定义的话,就可以有初始化)。

        数据抽象是通过:设计抽象数据对象来完成的(Data Abstraction is achieved through the design fo abstract data objects)。比如我们按需写了一个Stack,这个stack 包含了他的数据类型int,以及对他的操作pop, push等等~在使用的时候新建一个他的对象就可以使用了,这就是一种数据抽象。之后,我们发现int类型的Stack已经不能满足我们的需要了,也需要一个float类型的栈,于是,我们又想有更高一层的抽象,因为这两个类实在是太像了,于是所谓的类属程序设计就出来了,我们首先定义一个Stack<template>,这个template可以是int,float,char等等,我们需要那种类型的类,都可以由他生成一个对象,而这,也是一种数据抽象。

        与之直接相关的两个概念是:信息隐蔽和封装(Information hiding & encapsulation),这两个概念乍一看很像,但是, 请仔细看他们的定义:

1)Information hiding:It is the term used for the central principle in the design of abstractions: Try to hide as much information as possible from the component users.

2) encapsulation: The act of grouping together a set of data objects, together with the set of abstract operations, such that only the abstract operations are used to manipulate the data object.

        从他们的定义我们可以看出,信息隐藏是一个名词,是设计抽象的一个中心原则;而封装则是一个动词,使得外界只能通过规定的抽象操作来操纵数据对象,意味着这些数据被隐藏了,所以受封装包含了信息隐藏;但是信息隐藏却不一定包含封装,因为隐藏信息不一定要group data and operations together 。

        我记得刚学面向对象的时候,概念是很模糊的,就是觉得面向对象和之前的C没有什么区别,不明白类存在的意义是啥,觉得用结构体是一样的。但是,现在看来是不一样的。主要就是在于encapsulation:因为encapsulation is a question of language design,即,如果一个语言是不支持封装的,那么就没有办法封装,根本做不到“通过规定的抽象操作来操纵数据对象”。

        1977年底,“数据抽象”这个概念正式出现,其定义简单的说就是:一种基于封装的模块化机制。其本质特征就是使用与实现的分离。这样程序猿们就可以把大的系统分成很多小的部分,每个部分都有按所处理的数据而设计的接口,只用按照所需使用接口就可以了,具体实现则隐蔽起来。

        数据抽象类型ADT,是数据抽象的一种类型化实现机制。只有支持用户自定义ADT(当然,程序设计语言本身的基本类型都是ADT)的语言才是面向对象语言(这是判断一个语言是不是面向对象的标志之一)。ADT的出现为程序设计提供了许多好处:

模块化:每个ADT自成一个模块,使得程序的结构化得以保证的同时也使程序的编写简化

封装性和完整性:允许让什么数据进出受到了接口的控制,数据结构的完整性得到保存

简化了对正确性的检验:因为每个ADT是一个单独的模块,在检验的时候可以单独检验,编写主控程序的时候,对ADT只是调用的问题,那么主程序的的正确性检验就化简了

实现部分可以独立更换。

        这么看来,ADT的出现还真是伟大~~~~下一次课上多态,要好好听哦~



################################第四次课############################################# 

        这次课讲的是多态,老师在上节课的时候引提到了这个概念:什么是多态(polymorphsim)?为什么需要多态?以及他的种类(个人觉得这里好深奥委屈)。

        多态polymorpyism,就是说一种东西在很多地方都可以用,这是我的理解。比如,加法运算,int、float、double、char、string都可以用,这就是这个运算的多态。老师举的例子更多,除了我想到的之外,还说了3个:

1.水的不同形态:冰、水,水蒸气

2.对一组不同事物进行一致的处理:如红绿灯对交通车辆的统一管理

3.将一个集合上的操作施加于其子集

        对于软件来说,我们当然希望他支持多态啦,首先,很自然的,基本数学运算可以在程序中正常使用,某些通用简单好理解的标识符不再被语言的自有类型所独享!(上节课说的最后一个不公平!),更重要的,程序应该可以有大规模的重用了吧,,,想想啊,对不同类型有相同的处理~~而且用的地方越多,利用率就越高啊~~~而要想软件系统支持多态的前提,就是: 语言能够静态或者动态的确定类型!(因为如果一个语言不能确定一个变量的类型,那么多态这个概念也就没有了)

        多态的类型,大体上式universal和ad hoc ,跟具体的说,前者是对工作的类型不加限制,允许不同类型的值执行相同的代码(参数多态和包含多态),而后者则是对有限的类型,在执行具体的操作的时候,也得改改~(过载多态overloading和强制多态coercion)

        上节课提到的类属程序设计,就是参数多态,通过指定不同的实参来实现相同的功能。(在这里我们不禁想到,类属和ADT是类似的,一个是对于类型的抽象,一个是对于数据的抽象)

        包含多态则是说,一个操作可以作用于这个类型,以及他的子类型,都不用改的~一般要进行运行时的类型检查;

        过载多态,通常一个函数有不同的实现(参数变化),比如说我们在写一个类的时候,可能有很多种参数不同的构造函数,那么这就算是过载多态;

        强制多态,程序设计语言中基本类型的大多数操作符,在发生不同类型的数据进行混合运算时,编译程序一般都会进行强制多态,即强制类型转换~,当让,我们也可以自己显式的进行~casting(其原则是将值小的变成值集大的,对指针使用的时候要特别注意~)。

        除此之外,通常还认为有两个:一个是继承关系的(和包含多态不太一样,因为有可能会改变);另外一个是动态绑定机制(听说C++里面有virtual函数,目前还没有看到~)

        说了这么多种多态,我们不禁要想,多态的含义就是会说把一种操作作用在多种地方。那么,我们如何判断两种类型是不是相同的呢?也就是说他们可不可以使用多态,可以使用哪种多态? 而这 则是一个数学问题。

        我们是很早就接触代数这个概念的,而他在本质上也和我们的类型是一致的,简言之,都是 值集+操作集。数学家们当然早就发现了这一点,所以出现了λ演算、项包含等(陈老师说项包含他一直不太理解,据说是从“格”的概念引申出来的,想想就很有道理啊,自己似乎觉得格的概念就是继承机制的抽象,当然,我没有深究这个问题 )。

        Church 已经证明:图灵机、递归函数、λ演算和Post系统这四种计算模型是等价的,它们是计算机科学理论体系的支柱。  其中λ演算我个人觉得就是函数的模型,只不过我们通常的函数式自变量改变,函数本身不变,而这个可以是两者都改变 λf.λx.f(x),这样,就能够表示高阶的结构(到这里大家有没有什么想法大笑~~~比如我们的函数跳转表,过于多的switch case 语句是很烦~但是如果我们写一个函数指针数组,就可以简单快速的解决这个问题~这也是λ演算的实际应用)      

        这节课,老师第一次留了作业,要求写一个光亮课调节的灯,使用ADT。我的想法是值集部分包含两个量,一个是bool 类型的,表示开关状态,另外一个是enum类型的,表示灯的亮度;对外提高的操作有两个,一个是开关,就是每次对开关状态这个量取反;另外一个是亮度调节按钮,每次对亮度做取模运算(请忽略我这代码功底吧,,,)

class Light
{
	private bool state = false;
	private enum Lightness { Dark = 1, Normal = 2, Light = 3 };

	public void On_off()
	{
		if(state)
			state = false;
		else
			state = true;
	}

	public void brightness_adj(int count)
	{
		if(!state)
			return;
		else
		{
			Lightness = count%3+1;
			
			}
		}
	
	}
        下一次课要讲程序设计泛型,先开个头:啥叫 设计泛型?就是人们在编程是的惯用思路(老师说他决定了程序设计时采用的思维方式、工具)。比如,我在刚上大学的时候,接触到的就是面向过程的程序设计,就是顺序的执行一些函数以达到我们最终的目的。其本质是对过程的抽象,通过对数据分步骤的处理,一步一步达到我们的要求,可以说,这些都是可以画出流程图的。这期间,我认为算法是最重要的。因为过程的流程是相似的,算法的好坏直接决定了能否高效完成任务。我想这也是我们大部分人接受的吧。

        后来还有一些,比如模块化程序设计、函数程序设计(基于元数学的概念,给定一组规则,通过不断替换,得到一种最简单的表达。我怎么觉得这个很像编译器做的疑问)、逻辑程序设计(比如之前很早的人工智能语言,Prolog,就是基于一阶谓词演算的)、、、

        然后,伟大的OOP就出现了。(当年上课不好好听课,,,现在都是泪哭可以说,从一个面向过程的思维到面向对象的思维转变应该是挺不容易的,从过程调用的层次结构到现在的网状结构,我承认我现在才开始学习。)面向对象出现之初是ADT,随后而来的则是整个观念的转变:拿标准的概念来说:基于数据抽象、继承性和消息传递;以对实体进行分类、组织、协调作为思维主干。也就是说,以往,我们可能喜欢顺序的执行一个问题,想想每一步改做什么,但是,对于面向对象来说,我们则首先是把整个问题当成一个大的系统,这个系统是由相互协同的部分组成的,那么如何设计这些部分,他们之间要怎么通信等等都成为了我们要考虑的内容,而这恰恰是以前的思维所从来不考虑的。所以说面向对象对我们来说,不是仅仅将实现功能的函数封装在了类里面,更重要的设计思维才是写出高质量代码的关键!

下节课正式写OOP的设计泛型,今天先到这里1014.10.28(今天健身很开心~~年轻的生命还是应该要运动哒)


################################第五次课############################################# 

       

       这节课具体讲了OOP的5大基本概念:类、对象、继承性、方法和消息传递。这5个是基本概念,也就是说仅仅满足了OOP的基础要求,但是作为编程语言本身的机制来说,还需要其他的(比如重置overriding等,下节课讲)

        OOP的发展经历了两个过程:一个是以类为中心,另一个是以对象为中心。

        以类为中心是从类型=》ADT=》类=》继承性,然后依据现有的类和继承性,实例化出对象,对象通过消息传递机制进行通信。这是软件工程师的惯用思维。而以对象为中心的则是最初稿人工智能的那些人们常用的,通过现实世界中很多的对象抽象出类,由类在抽象出类型等。

        程序员们还是比较喜欢第一种思维的(我猜这会让人有一种掌控的快感)。但是这也就要求了在写代码的时候,一定要提前设计好!不要轻易去写,要写就写个漂亮的程序!而要考虑的问题很多,包括:

怎样区分不同种类的对象?

怎样表示对象的存储结构?

怎样表示一种对象可以承担的计算任务?(对象的行为)

怎样表示类之间的协同方式,即通信协议(语法、语义和时序。语法讲了数据的格式,语义讲了信息的内容和控制信息等,时序则强调了通信的顺序其实这三个意思融合起来就是 who send what to whom 的问题)

怎样表示和利用类与类在结构和行为之间的相似性?(继承性的具体使用:相同的拿来用,不同的可以具体改正)

怎样利用不同类或者对象的相似性,对程序的重用、扩充和修改提供支持?

        首先,我们从类开始看。在面向对象中,类就是个支持继承和多态的抽象数据类型,具有实例化能力。使用它的时候通常是通过生成他的对象,使用它对外定义的接口。所以说设计好的话,我们使用起来就应该像使用系统的基本类型一样,可以变化出无穷的花样(转圈圈~~应该以设计出这样的类为目标奋斗)。下面,我做一下具体的对比:

类                                                                              系统基本类型

能见度   能看到类型名和一组操作名 能知道这个类型,并且可以使用它的一些基本操作

 (包括操作名、参数、操作含义和使用规则)不知道内部的具体数据结构,这些操作通俗易懂

   但是这个类的具体实现机制我们不清楚,是我们熟悉的,看到符号或者名字我们就可以知道是

   也不知道操作的具体实现                                                 做什么的,但是也不知道他具体是怎么做的

使用方法 实例化出对象定义变量

        所以说类这个概念和系统的基本类型本质上是一眼的都是ADT。一回归到ADT,就涉及到了访问权限、初始化(防止程序设计的悲哀——对不确定的事情做了确定的操作!)和一些善后处理(释放在使用时动态申请的空间)。这也很好理解,比如我们在写程序的时候,需要动态申请一些资源,例如malloc申请内存,那么我们程序在退出的时候,指向这块内存的指针被释放掉了,可是这块内存却并没有还给系统的话!(至于为什么程序结束那些变量什么的可以自动清除但是这些我们主动分配的确不行是因为:他们存的位置不一样!前者在栈,后者在堆)。所以我们在设计类的时候,就要想好,他是给谁使用的,我们希望使用者使用它的那些功能,这就是访问权限问题;初始化,给她赋什么值(显示的执行new 操作,编译器将改声明语句翻译成这个类的constructor的一次调用,并按照他所需要的空间分配一块连续的内存)?在不想使用的时候,别忘了释放当初使用的heap里面的东西,这应该是程序设计的一个基础素质。

总的来说,类表示了一组对象的结构、行为和通信协议,而忽略了具体对象的值。在运行的时候,只有用他实例化的对象才可以产生行为保持和改变状态。

        对象:对象是类的实例。同一个类产生的对象具有相同的数据结构、操作集合和能见度,不同的标识,相同或者不同的状态(值),拥有和保持不一样的运行状态。很类似我们使用变量。但是他们不同的。

        简单说,对象是一种“主动数据”,而变量则是被动数据。因为对象之间是通过消息传递来交互的,而变量之间则是通过一些过程与函数来改变。想想看,我们的基本类型定义的操作,都是对他产生某种变化,而对象的方法则更像是两个人的通信过程,有很强的交互性。

        而且凡是类似通信过程,就可以有同步通信和异步通信(打电话和发短信的区别),可以做到主动询问。这从对象的角度很好理解,但是传统的面向过程是很难做到的。而对象之间怎么关联,怎么通信则是我们设计的难点(对象自己的数据结构,消息传递的参数,全局关联表和相应对象管理操作)。

      总结:  不同的对象有不同的状态(值),意味着他们有独立的数据空间;但是他们确共享操作代码(以节省代码空间和保持操作的一致性)。此外如果有大家都需要共享的东西,不同对象使用的时候也不变,那么就设置成静态成员(C++中的static,存放在堆中)。此外,大部分面向对象语言为对象分配空间都是连续的,方便继承的使用(这是因为继承是采用函数指针来实现不同函数的选择的,地址连续方便指针的操作)。

        接下来讲消息传递机制和方法。这是实现对象为主动数据的途径!而要做到两个实体之间的通信,必要条件是:

        信道(至少一条;可以是多条,比如,很多人不只有1部手机,那么就可以实现同时和好几个人打电话的要求)

        遵循同一种通信协议(前面提到过,通信协议涉及:语法‘语义和时序;在这里,语法就好比参数,类似我们打电话都得靠电话号码去呼叫对方;语义则是具体内容的要求,比如我们打电话说的语言一定是我们双方都懂的;时序就是一方在说的时候另一方面通常是听而不能说,就是说通信在任何一个短的瞬间都是一个发消息一个收,而不能同时),即:

        who send what to whom, in what way.

        因为消息传递协议是类定义的,所以,一个消息传递的协议/合法范围是早就规定好的,通信方式是可预知的。

        和过程调用比起来,消息传递机制要显示的指明接收方。这样就提供了一种安全机制:使接收方显示的表示为消息所规定的操作承受对象(相比较过程调用,信道则是隐含的,只要是语法符合,便可以无条件的调用)。除此之外,通信可以适用于并发和分布式环境。试想,加入没有消息传递机制,那么并发和分布式应该是让大量的原始数据不停的在跑,而且不能是异步的!这对流量来说也是不合理的!

        我们在使用的时候,使用’.‘这个操作符。这个操作符即表达了最原始的通信原语。例如 e1.changeAge(25);e2.changeage(61)等。我们在使用的时候没有感到任何疑议,但是他其实提供了一种双重作用:一个是抽象于具体对象(为什么都是对年龄的改变,一个是25,一个是61,程序怎么区分?)另一个则是关联与具体的对象(不同的对象调用不会混乱)。其实我们上面提到过,类型在存储的时候,会给每个实例化出的对象分别分配存储空间,但是操作则是代码共享的。在调用这些操作的时候,使用了一种叫做自引用(self reference)的机制。在C++中叫做this(这本质是一种C* 指针,表示指向一个C类型的指针),编译器在遇到消息传递的时候,比如上面的e1.changeAge(25),就会翻译成:changeAge(&e1,25),里面在改年龄的时候用的就是this->age;这也就实现了我们上面所说的双重作用——类型决定了共享方法体的对象范围,即this所指向的;其具体的值this->决定了与特定对象相关。(这是软件技术中常用的一个方法——让一个程序成分的值和类型同时起不同的、相关的作用

        说点别的。从上面我们也可以看出,运行时的C++和C其实是一样的——this传入的对象其实就是一个指针啊。不过这个机制是很赞的,因为他解决了两个问题。老师说过,如果一个设计能够做到满足两个要求,那就相当的牛了。

        接下来讲讲OOP的另外两个基本概念:继承性和类的层次结构。

        再回到我们面向对象之初,想想为什么需要他。当我们从一个类似流水线的数据加工的面向过程的思维转变到以设计系统为主的OOP思维的时候,现实世界就从单一的层次结构变成了网状结构了。类和对象就好比是这个网络上的一个一个节点,孤立的类只能描述实体集合的特征同一性,然而客观世界的实体往往不是非此即彼的,而是充满相似性的(这种相似性可以是涵义上的,也可以是结构上的)——既有共同点,又有差异性。

        为了比较高效的解决这个问题,继承性就出现了,他是OOP技术基于差别开发的一种主要机制:在类之间既能体现其功效和差别,又能给出这个信息,还能将这个信息按照需要进行传递。有单程继承和多重继承之分。单重继承的数据结构是,多重继承的数据结构是。而之所以选择这两种结构,是因为只有这两种结构是具有层次性的!因为只有树和格中,任何一个节点到其父节点的路径都为1,这样就不会有二义性:无论是自上而下的实例生成还是自下而上的抽象,都可以找到确定的路径。并且,这种结构也决定了我们在使用constructor和destructor的时候的顺序!

        不过对于计算机来说,但是继承的传递性增加了类之间的耦合度(慎用!有人说7是个很神奇的数字,,,据说传递性在7层是最好的,但是就我目前的水平来说,,,哭)所以尽量少用。尤其是多重继承(Java中已经取消了类的多重继承,但是用接口来实现),老师还说,使用多重继承不可能代替对象的组装设计这句话什么意思还没有想清楚。

        对于学生来说,考试经常会很无聊的考到继承时候能见度的问题。这里的能见度包括三个个概念:对外部的能见度(信息隐藏的体现),对子类的能见度和子类的继承方式。简单的来说,子类可以看见父类除了private之外的所有(儿子除了老子的隐私,啥都可以看);而继承方式则是:public继承,那么子类对外的能见度和父类一样;protected 继承则只是将原来protected变成private;private继承则对外没有能见度(private 陷阱?)。

        总结:理解OOP中的概念不要局限在串行、单进程上,要从并发、通信,分布式等思想来看。消息传递机制和过程的最大差别在于——既要动态的与对象相关,同时又要静态的和特定对象无关,核心实现机制是自引用。而继承则尽量少用,尤其是新手。


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值