《Essential C++》学习笔记

书籍介绍

大名鼎鼎的《C++ Primer》作者为了照顾想快速入门C++的"初学者"而编写的入门书籍,全书不足300页却包含C++的大量重要知识,关于此书的由来读者可以自行去阅读此书的前言,这里就不多提啦。
该书封面

此文由来

关于这个文章,其实也是我自己给我的一个挑战,这本书虽然精简,短短不足300页,但是大量的语法,原理一笔带过,很难在几句话的功夫理解一类知识的思想,换句话说,《C++ Primer》太长,很多人读不下去,而这本书太短,又需要一定的注解,解释来辅助理解,所以这篇文章由此诞生,我希望通过这个机会,让我好好理解一遍书中的知识,3月份开学后,再来挑战巨头 C++ Primer。
本人第一次写博客 基础知识过于简陋 要是有什么在知识上根本性的失误或者语句不通顺等等任何问题,请告诉我,我们一起进步!

警告

此书是为学过C(或者其他编程语言),想快速过渡到C++的人准备的,完全零基础的人看这本书会看得云里雾里,所以很不建议纯新手阅读此书。
本书的第一、二个章节,我会尽可能地省略,因为我已经认为你学过C,并且有一点点(或者很多?)面向对象语言基础了。所以,让我们开始吧!

第一章:C++编程基础

类似于表达式 数组 条件语句 循环语句这种C语言都有的我不再说了

关于Vector

  • Vector,它是一个多功能的,能够操作多种数据结构和算法的模板类和函数库。
  • vector,是一个能够存放任意类型的动态数组,能够增加和压缩数据。 1

在我的理解下 它就是一种很方便的,可以自定义数据类型的这么一种工具

  • 如果想要定义vector object 必须要包含vector头文件
#include<vector>

第二章:面向过程的编程风格

指针和引用的区别:

  • 在本质上来说 指针和引用都是变量 存放的都是被引用对象的地址
  • 指针变量本身可以被寻址
int a =100;
int  *p= &a;//p存放的是a的地址
int **p1 = &p;//p1存放的就是p的地址
  • 而引用变量地址却不可被寻址,假如引用变量为r,&r操作得到的只能是r所指向对象的地址,而不是r本身的地址。
  • 数组元素允许是指针常量,而不能是引用
  • 例如 a作为一个引用数组 a[0]=1; 无法确定是a[0]的值为1还是a[0]所引用的值为1,容易产生二义性。
  • 引用不能为空,而指针可以为空。你可以只声明一个指针变量,而不去给它赋值。一个未指向任何对象的指针,其地址值为0.
  • 指针可以有多级,而引用只能一级
  • sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小

所以对于以上来说,当我们对指针进行解引用操作时(*p),一定要确定其值并非0,对于引用来说,因为它一定会代表某个对象,所以不需要做这样的检查

堆内存

我们在学习C语言的过程中,对于链表这个数据结构来说,经常用到"malloc"这个函数,用来为一个新结点分配空间。在使用结束之后,又会手动删除这个空间,而在JAVA和C++中,这个函数变成了new,(他们俩的区别不在此文范围内)。这就是所谓的堆内存

  • 堆允许程序在运行时动态地申请某个大小的内存空间。
  • 在C++中,通过new出来的对象,需要使用delete加以释放。

inline函数

inline存在的意义:用来优化C语言的宏定义(或者说 替代它)
在C语言的宏定义使用预处理器实现,所谓预处理器就是在真正的编译开始之前的一段程序,所以C语言的宏定义效率是十分高的。
但是C++引用了类机制,也就导致了有些成员在面对宏定义时,是十分尴尬的。
此外 inline函数可以解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题
以下以此段c代码为例

#include <stdio.h>
//函数定义为inline即:内联函数
inline char* f(int a) {
    
    
    return (i % 2 > 0) ? "奇" : "偶";
} 

int main()
{
    
    
   int i = 0;
   for (i=1; i < 100; i++) {
    
    
       printf("i:%d    奇偶性:%s /n", i, f(i));    
   }
}

普通情况运行的时候,系统通过循环要一次次调用f函数的。
使用inline之后,每次运行相当于在把printf()里的f(i)调用直接换成了return (i % 2 > 0);这样就提高了运行效率

  • 也就是说,在函数体很小的情况下,使用Inline函数可以提高效率
  • 需要注意的一点是,将函数定义为inline,仅仅是对编译器的一种建议,编译器是否执行这个请求,需视编译器而定。

inline函数虽好,但是需要慎用

  • 内联是以代码膨胀复制为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码, 将使程序的总代码量增大,消耗更多的内存空间。
  • 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
  • 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

参考文章:inline函数的总结

第三章:泛型编程风格

关于STL

顺序型容器 vector与list

  • vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。

但是我们都很清楚,这种数据结构带来的后果就是查找效率很高,为 O(1)。但是插入和删除就比较麻烦。

  • 而vector和java里的ArrayList一样,都具有自动扩容的功能。

不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。

  • list底层由双向链表实现,因此内存空间是不连续的。所以插入时间复杂度低,查找时间复杂度高。

在C语言里是最底层的数组和链表,到了C++里就变成vector和list。真是走到哪掐到哪啊。

关联容器 map和set

其实如果大家用过java,里面也有类似的数据结构,map一般表示一对对的key/value组合,俗称键值对。而set就只含有key。

  • set不允许键值重复,可以用此解决一些算法问题,例如环形链表问题。
  • map也不允许键值重复,但是map允许修改键对应的值。set则不能修改键值。

set和map底层都是红黑树,红黑树是一种具有自动平衡功能的二叉树,(具体实现比较困难,有兴趣的朋友可以自己查阅相关资料),在set中,如果想要修改键值,那么就会破坏红黑树的结构, 所以STL中将set的迭代器设置成const,不允许修改迭代器的值。

Iterator(迭代器)

迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。
有了迭代器前提是要有与之匹配的容器,那么我们在代码中实现一下:

vector<int> v;//声明一个int类型的可变长数组
vector<int>::iterator i;//定义一个迭代器

我们用书里的说法解释一下声明迭代器的代码:
此处 i 被定义为一个 iterator,指向一个 vector,后者的元素类型为 int

定义完迭代器之后,就可以进行迭代操作了

 for (i = v.begin(); i != v.end(); ++i)   //用迭代器遍历容器
        cout << i << " ";  //*i 就是迭代器i指向的元素

Funtion object(函数对象)

不知道是不是我阅读能力太差 从这里开始 我感觉书中的内容就很难读得懂了
函数对象可以当成一种运算符重载。思想是:用类来封装一个函数功能,然后用这个类实例化不同的函数对象。

 class  Add {
    
    
    public:
        int operator()(int a1, int a2){
    
    //重载"( )"运算符实现加法功能
            return a1+a2;
        }
} ;
 int  a =1  ,b = 2 ;
Add add; //实例化add对象
cout << add(a1,a2) << endl;  

第四章:基于对象的编程风格

This指针

用过JAVA的朋友们对this这个概念一定不陌生,现在我们就从C++的角度解释一下this指针。
以上思路来自C++ this指针(直戳本质)

class Theshy  //C++代码
{
    
    
public:
    int num;
    void SetNum(int p);
};
void Theshy::SetNum(int p)
{
    
    
    num=  p;
}
int main()
{
    
    
    Theshy obj;
    obj.SetNum(20000);
    return 0;
}
struct Theshy  //C代码
{
    
    
    int price;
};
void SetNum(struct Theshy* this, int p) //不一样的地方 
{
    
    
    this->price = p;
}
int main()
{
    
    
    struct Theshy shy;
    SetNum(&shy, 20000);
    return 0;
}

这两段不同语言的代码作用是一样的,比较他们的区别,最大的区别就是在SetNum()中,C++多了一个参数, “struct Theshy *this”,这就是C++中的this指针,在这里编译器是做了隐式处理。

this指针是在成员函数中用来指向其调用者(一个对象)

Static关键字

老熟人了,但是static关键字用处很多。我们从它的引入开始。在一个函数内部定义的变量,当编译器执行到这个函数中时,编译器会在栈中为变量分配空间,直到这个函数结束。如果想让这个变量保存,我们在C语言中会使用全局变量来操作,但是C++具有类,具有访问权限,使用全局变量会导致访问冲突。这个时候就可以用static用来修饰类的数据成员,表明对该类所有对象这个数据成员都只有一个实例。即该实例归所有对象共有。
static的作用(在C/C++)

  • 修饰某个变量的时候,只初始化一次,延长了局部变量的生命周期。
  • 如修饰函数在栈空间存放的数组,不想被释放,就可以用static。

static的特点

  • 静态变量都在全局数据区分配内存
  • 未经初始化的静态全局变量会被程序自动初始化为0

下面介绍static在C++中的独有用法

  • 被 static 修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要 new 出一个类来
  • 被 static 修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要 new 出一个类来
  • 在 C++ 中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。

static除了修饰成员变量之外,还可以修饰函数,叫做静态成员函数。普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。

  • 普通成员函数拥有我们上面讲到的this指针,而静态成员函数没有。
#include <iostream>
using namespace std;
class Shop
{
    
    
public:
    Shop(int size);
    void ShowSize();
    static void ShowPrice(); //声明静态成员函数用来显示价格
    static int ChangePrice(int price); //声明静态成员函数用来更改价格
private:
    int m_size; //声明一个私有成员变量
    static int m_price; //声明一个私有静态成员变量
};
Shop::Shop(int size)
{
    
    
    m_size = size;
}

void Shop::ShowSize()
{
    
    
    cout << "商品数量:" << m_size << endl;
}
void Shop::ShowPrice()
{
    
    
    cout << "商品价格:" << m_price << endl;
}
int Shop::ChangePrice(int price)
{
    
    
    m_price = price;
    return m_price;
}
int Shop::m_price = 100; //初始化静态成员变量

int main(int argc, char* argv[])
{
    
    
    Shop::ShowPrice();
    Shop::ChangePrice(200);
    Shop::ShowPrice();
    Shop shop(50);

    shop.ShowSize();
    shop.ShowPrice();

    return 0;
}

深究

在头文件把一个变量申明为static变量,那么引用该头文件的源文件能够访问到该变量吗?

  • 可以。声明static变量一般是为了在本cpp文件中的static变量不能被其他的cpp文件引用,但是对于头文件,因为cpp文件中包含了头文件,故相当于该static变量在本cpp文件中也可以被见到。当多个cpp文件包含该头文件中,这个static变量将在各个cpp文件中将是独立的,彼此修改不会对相互有影响。

为什么静态成员函数不能申明为const?

  • 这是C++的规则,const修饰符用于表示函数不能修改成员变量的值,该函数必须是含有this指针的类成员函数,函数调用方式为thiscall,而类中的static函数本质上是全局函数,调用规约是__cdecl或__stdcall,不能用const来修饰它。一个静态成员函数访问的值是其参数、静态数据成员和全局变量,而这些数据都不是对象状态的一部分。而对成员函数中使用关键字const是表明:函数不会修改该函数访问的目标对象的数据成员。既然一个静态成员函数根本不访问非静态数据成员,那么就没必要使用const了

为什么不能在类的内部定义以及初始化static成员变量,而必须要放到类的外部定义

  • 因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。

为什么静态成员函数只能访问静态成员变量。

  • 1.静态成员函数只属于类本身,随着类的加载而存在,不属于任何对象,是独立存在的
  • 2.非静态成员当且仅当实例化对象之后才存在,静态成员函数产生在前,非静态成员函数产生在后,故不能访问
  • 3.内部访问静态成员用self::,而访问非静态成员要用this指针,静态成员函数没有this指针,故不能访问。

静态成员的作用、优点

  • 静态成员函数主要为了调用方便,不需要生成对象就能调用。

第五章:面对对象的编程风格

多态与虚函数

说到虚函数,不得不提到多态
什么是多态

多态:允许将子类类型的指针赋值给父类类型的指针。

这样实现了一个函数会根据传入参数的不同有不同的功能。
以下思想来自知乎,名为乌索普的回答。

void Attack (Hero *h);  

如果没有多态,这个函数,只能攻击Hero这个类的对象,但是Hero是一个大类,里面包括Zed(劫),Fiora(剑姬)。你想攻击劫和剑姬,普通方法是创建zed对象继承Hero类,然后在类中加一个Attack函数,参数为zed。但是,联盟现在有一百多英雄,你现在要一个个去加嘛?如果学过一点设计模式,看到这种冗杂的代码是会嗤之以鼻的。那么该怎么做?

创建Zed类去继承Hero类
传入到Hero函数中

好了,你并没有额外编写Attack函数,这是利用多态完成了这个功能。
但是继承时,子类也会把Attack函数继承过来,现在子类和父类就都有同名同参数函数了,那么现在的问题是:

  • 如何确定指针调用的函数是子类还是父类的?
Hero *h = new Zed()
h->back() //回城函数 

在程序编译阶段,对象还没有产生时,程序只能根据指针的类型来判断调用哪个函数,所以无论h指向什么对象,back函数都是父类版本的back函数。这时还没有用到多态。

这个在编译时决定函数是哪个类的函数的方式就叫做静态联编

有静态就有动态,想调用子类的函数的方式就是动态联编。
那么如何把静态联编变成动态联编呢?
刚刚说到,编译器在静态联编时调用的是父类的函数。
h->back 在编译器的作用下变成了 Hero::back(h)
这时就要用到我们的虚函数了(终于提到虚函数了)
当back函数声明成虚函数时,就不会上述转换了,而是转化为

h->back---------->(h->vtpl[1] (h))

这个vtpl就是虚表指针
当类中出现一个virtual指针时编译器就自动在类的成员变量里加入一个指针成员。
这个指针成员指向这个类的虚表指针。
当子类重写父类方法时 同时也是把继承自父类的虚表中对应函数的索引的函数指针从父类函数改成了自己的函数。这就造成了子类和父类对象里面的虚表内容不同。所以动态联编时 去子类 或者父类里面找虚表,调用的函数不同。这也就完成了多态。

我们再写一段代码帮助理解:

#include<iostream>
using namespace std;

classA{
    
    
public:
A(){
    
    };
~A(){
    
    };
void show()
{
    
    
cout<<"A"<<endl;
}
};

classB:public A{
    
    
public:
B(){
    
    };
~B(){
    
    };
void show(){
    
    
cout<<"B"<<endl;
}
};
int main()
{
    
    
A *p=new B;
p->show();
return 0;
}

在这种情况下,程序会输出A。这就是静态联编的情况,如果我们在8和18行写上
virtual,程序就会输出B了。

第六章:以template(模板)进行编程

详解C++模板

第六章整个一章讲都是模板,那么我们就多说点。
比如说我们要定义一个函数,这个函数可以实现多种类型数据比较大小的功能

int max(int x,int y);  
{
    
    return(x>y)?x:y ;}


float max( float x,float y){
    
    
return (x>y)? x:y ;}

我们用到了函数重载,但是数据类型有很多,如果每一个类型都要写的话,工作量比较大(不过重载函数真的很多就是了,我在用Unity看他们的API时,动不动就十几种重载方法)
这个时候就用到我们的C++模板了

模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。

以下为实现一个求最小值函数模板

#include <iostream>
using namespace std;

template<class T>
T min(T x,T y)
{
    
    
return (x<y?x:y);
}
void main( )
{
    
    
     int a1=2,a2=10;
     double d1=1.5,d2=5.6;
     cout<< "较小整数:"<<min(n1,n2)<<endl;
     cout<< "较小实数:"<<min(d1,d2)<<endl;
     system("PAUSE");
}

输出2和1.5,达到了我们的要求。

  • 模板可以显著减小源代码的大小并提高代码的灵活性,而不会降低类型安全。

  • 编译器由模板自动生成函数的过程叫模板的实例化

例如:

template<class T>
void Swap(T & x, T & y)
{
    
    
    T tmp = x;
    x = y;
    y = tmp;
}
//以上省略 
int n = 1, m = 2;
    Swap(n, m);  //编译器自动生成 void Swap (int &, int &)函数

当编译器运行到Swap,会用 double 替换 Swap 模板中的 T,自动生成替换完的Swap代码

  • 由模板实例化而得到的函数称为模板函数。在某些编译器中,模板只有在被实例化时,编译器才会检查其语法正确性。如果程序中写了一个模板却没有用到,那么编译器不会报告这个模板中的语法错误。

编译器对模板进行实例化时,并非只能通过模板调用语句的实参来实例化模板中的类型参数,模板调用语句可以明确指明要把类型参数实例化为哪种类型。可以用:

模板名<实际类型参数1, 实际类型参数2, …>

#include <iostream>
using namespace std;
template <class T>
T In(int n)
{
    
    
    return 1 + n;
}
int main()
{
    
    
    cout << In<double>(4) / 2;
    return 0;
}

Inc(4)指明了此处实例化的模板函数原型应为:double In(double); 因此编译器不会因为实参 4 是 int 类型,就生成原型为 int Inc(int) 的函数。上面程序输出的结果是 2.5 而非 2。

模板基本分为两种类型,函数模板和类模板。
类模板的语法格式为:

template<class 形参名,class 形参名,…>
    class 类名{ … };

模板的非类型形参

什么是非类型形参?顾名思义,就是表示一个固定类型的常量而不是一个类型。

template<class T,int MAXSIZE> class List{
    
    
private:
T elems[MAXSIZE];
public:
Print(){
    
     cout<<"The maxsize of list is"<<MAXSIZE; }
}
List<int,5> list;
list.Print();//打印"The maxsize of list is 5"

这个固定类型是有局限的,只有整形,指针和引用才能作为非类型形参,而且绑定到该形参的实参必须是常量表达式,即编译期就能确认结果。

第七章 异常处理

感谢你看到了这里,古语有言:行百里者半九十,文章历程也只有一半而已(笑),让我们一鼓作气,把这最后的一点知识完成吧。
异常的几个关键字和JAVA是一样的,都离不开那几个。try,catch,throw
try用来放置可能抛出异常的代码,而catch用来捕获抛出异常的代码。

抛出异常的代码

double division(int a, int b)
{
    
    
   if( b == 0 )
   {
    
    
      throw "Division by zero condition!";//用到了throw语句
   }
   return (a/b);
}

捕获异常的代码

try
{
    
    
   // 保护代码
}catch( ExceptionName e )
{
    
    
  // 处理 ExceptionName 异常的代码
}

下面我们实现一个除以0的异常处理

#include <iostream>
using namespace std;
 
double division(int a, int b)
{
    
    
   if( b == 0 )
   {
    
    
      throw "Division by zero condition!";
   }
   return (a/b);
}
 
int main ()
{
    
    
   int x = 50;
   int y = 0;
   double z = 0;
 
   try {
    
    
     z = division(x, y);
     cout << z << endl;
   }catch (const char* msg) {
    
    
     cerr << msg << endl;
   }
 
   return 0;
}

我们定义了一个类型为const char*的异常,如果b等于0,抛出异常,被try察觉,被catch捕获到,然后就会看见控制台输出Division by zero condition!

catch(…)可以捕获任意类型的异常,当不知道什么类型的异常时,用这个可以防止程序崩溃。

	catch (...)  
	{
    
    
		cout << "未知异常" << endl;
	}

栈展开

栈展开指的是:当异常抛出后,匹配catch的过程。

抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。沿着函数的嵌套调用链向上查找,直到找到一个匹配的catch子句,或者找不到匹配的catch子句。

异常的重新抛出

void test() {
    
    
	int *p = new int[10];	
	
	//f抛出的异常类型不知道
	//而且若不处理代码会崩溃,之后会内存泄露
	try {
    
    
		f();//里面可能有异常,要对该里面的异常进行捕获
	}
	catch (...)    //进行捕获并且再往出抛,主要目的是为了释放资源等等
		           //相当于上一个异常继续往出抛 在主函数中用 int就可以接收
	{
    
    
		delete[] p;  //在结束之前,释放资源
		throw;   // 重新抛出
	}		
}
int main() {
    
    
	try {
    
    
		test();
	}
	catch (int error)
	{
    
    
		cout << error<< endl;  
	}
	system("pause");
	return 0;
}

异常的优点:

  1. 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。
  2. 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
  3. 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
  4. 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。

异常需要的几个注意点:

  • 如果异常抛出是用的 int 类型 而在外部捕获中 捕获函数是用的char类型进行的捕获那么,该错误就不会被捕获到,而代码也就此终止异常继续往外传。
  • 性能问题。这个一般不会成为瓶颈,但是如果你编写的是高性能或者实时性要求比较强的软件,就需要考虑了。
  • 指针和动态分配导致的内存回收问题:在C++中,不会自动回收动态分配的内存,如果遇到异常就需要考虑是否正确的回收了内存。在java中,就基本不需要考虑这个。
  • . 函数的异常抛出列表:java中是如果一个函数没有在异常抛出列表中显式指定要抛出的异常,就不允许抛出;可是在C++中是如果你没有在函数的异常抛出列表指定要抛出的异常,意味着你可以抛出任何异常。
  • C++中编译时不会检查函数的异常抛出列表。这意味着你在编写C++程序时,如果在函数中抛出了没有在异常抛出列表中声明的异常,编译时是不会报错的。
  • 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟 踪调试时以及分析程序时,比较困难。

部分代码及思想出自:
C++异常处理
C++异常以及优缺点
C++异常详解

总结

在规划这篇文章的时候,我给自己完成的目标是十几天,可没想到第三天就完成了,最大的不同是,原本我想把《Essential C++》这本书再读一遍,然后把里面晦涩难懂的地方挑出来,说给大家听,可没想到,后来我真的看不下去了(手动狗头),于是就按照目录,把我没接触过的,或者说之前没搞懂的知识点,通过网络的强大力量,过了一遍。这里我主要是有两点考虑,开学之后我还要研读《C++ Primer》,所以很多疑惑与不解,留到那个时候再说。其次,作为一个学生,把每一个知识点的每一个细节都学会,只能说异想天开(我是这么认为的),甚至我认为这是不可完成的任务。学那么暂时用不上,不如好好牢固最基础的知识。
这是我的第一篇博客,肯定会有很多很多的失误,如果大家耐心看到这里,并且发现了错误的话,请在评论区积极地和我对线(欢迎欢迎)。文章有一万多字,其中很多字都是借鉴别人的(奈何本人实力太弱)。但是第一篇博客就写一万字,这对我来说也是成就感颇深的一件事。这是第一篇,但不是最后一篇,希望我们在下一篇文章见!
完结撒花!!!


  1. 桂珠、张平、陈爱国 .Java面向对象程序设计(jdk1.6)第三版:北京邮电大学出版社,2005 ↩︎

猜你喜欢

转载自blog.csdn.net/m0_50816320/article/details/113815001