大名鼎鼎的《C++ Primer》作者为了照顾想快速入门C++的"初学者"而编写的入门书籍,全书不足300页却包含C++的大量重要知识,关于此书的由来读者可以自行去阅读此书的前言,这里就不多提啦。
关于这个文章,其实也是我自己给我的一个挑战,这本书虽然精简,短短不足300页,但是大量的语法,原理一笔带过,很难在几句话的功夫理解一类知识的思想,换句话说,《C++ Primer》太长,很多人读不下去,而这本书太短,又需要一定的注解,解释来辅助理解,所以这篇文章由此诞生,我希望通过这个机会,让我好好理解一遍书中的知识,3月份开学后,再来挑战巨头 C++ Primer。
本人第一次写博客 基础知识过于简陋 要是有什么在知识上根本性的失误或者语句不通顺等等任何问题,请告诉我,我们一起进步!
此书是为学过C(或者其他编程语言),想快速过渡到C++的人准备的,完全零基础的人看这本书会看得云里雾里,所以很不建议纯新手阅读此书。
本书的第一、二个章节,我会尽可能地省略,因为我已经认为你学过C,并且有一点点(或者很多?)面向对象语言基础了。所以,让我们开始吧!
类似于表达式 数组 条件语句 循环语句这种C语言都有的我不再说了
在我的理解下 它就是一种很方便的,可以自定义数据类型的这么一种工具
#include
int a =100;
int *p= &a;//p存放的是a的地址
int **p1 = &p;//p1存放的就是p的地址
所以对于以上来说,当我们对指针进行解引用操作时(*p),一定要确定其值并非0,对于引用来说,因为它一定会代表某个对象,所以不需要做这样的检查
我们在学习C语言的过程中,对于链表这个数据结构来说,经常用到"malloc"这个函数,用来为一个新结点分配空间。在使用结束之后,又会手动删除这个空间,而在JAVA和C++中,这个函数变成了new,(他们俩的区别不在此文范围内)。这就是所谓的堆内存
inline存在的意义:用来优化C语言的宏定义(或者说 替代它)
在C语言的宏定义使用预处理器实现,所谓预处理器就是在真正的编译开始之前的一段程序,所以C语言的宏定义效率是十分高的。
但是C++引用了类机制,也就导致了有些成员在面对宏定义时,是十分尴尬的。
此外 inline函数可以解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题
以下以此段c代码为例
#include
//函数定义为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函数的总结
但是我们都很清楚,这种数据结构带来的后果就是查找效率很高,为 O(1)。但是插入和删除就比较麻烦。
不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
在C语言里是最底层的数组和链表,到了C++里就变成vector和list。真是走到哪掐到哪啊。
其实如果大家用过java,里面也有类似的数据结构,map一般表示一对对的key/value组合,俗称键值对。而set就只含有key。
set和map底层都是红黑树,红黑树是一种具有自动平衡功能的二叉树,(具体实现比较困难,有兴趣的朋友可以自己查阅相关资料),在set中,如果想要修改键值,那么就会破坏红黑树的结构, 所以STL中将set的迭代器设置成const,不允许修改迭代器的值。
迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。
有了迭代器前提是要有与之匹配的容器,那么我们在代码中实现一下:
vector<int> v;//声明一个int类型的可变长数组
vector<int>::iterator i;//定义一个迭代器
我们用书里的说法解释一下声明迭代器的代码:
此处 i 被定义为一个 iterator,指向一个 vector,后者的元素类型为 int
定义完迭代器之后,就可以进行迭代操作了
for (i = v.begin(); i != v.end(); ++i) //用迭代器遍历容器
cout << i << " "; //*i 就是迭代器i指向的元素
不知道是不是我阅读能力太差 从这里开始 我感觉书中的内容就很难读得懂了
函数对象可以当成一种运算符重载。思想是:用类来封装一个函数功能,然后用这个类实例化不同的函数对象。
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;
用过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(­, 20000);
return 0;
}
这两段不同语言的代码作用是一样的,比较他们的区别,最大的区别就是在SetNum()中,C++多了一个参数, “struct Theshy *this”,这就是C++中的this指针,在这里编译器是做了隐式处理。
this指针是在成员函数中用来指向其调用者(一个对象)
老熟人了,但是static关键字用处很多。我们从它的引入开始。在一个函数内部定义的变量,当编译器执行到这个函数中时,编译器会在栈中为变量分配空间,直到这个函数结束。如果想让这个变量保存,我们在C语言中会使用全局变量来操作,但是C++具有类,具有访问权限,使用全局变量会导致访问冲突。这个时候就可以用static用来修饰类的数据成员,表明对该类所有对象这个数据成员都只有一个实例。即该实例归所有对象共有。
static的作用(在C/C++)
static的特点
下面介绍static在C++中的独有用法
static除了修饰成员变量之外,还可以修饰函数,叫做静态成员函数。普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。
#include
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变量,那么引用该头文件的源文件能够访问到该变量吗?
为什么静态成员函数不能申明为const?
为什么不能在类的内部定义以及初始化static成员变量,而必须要放到类的外部定义
为什么静态成员函数只能访问静态成员变量。
静态成员的作用、优点
说到虚函数,不得不提到多态。
什么是多态?
多态:允许将子类类型的指针赋值给父类类型的指针。
这样实现了一个函数会根据传入参数的不同有不同的功能。
以下思想来自知乎,名为乌索普的回答。
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
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了。
第六章整个一章讲都是模板,那么我们就多说点。
比如说我们要定义一个函数,这个函数可以实现多种类型数据比较大小的功能
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
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
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 类名{ … };
什么是非类型形参?顾名思义,就是表示一个固定类型的常量而不是一个类型。
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
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;
}
部分代码及思想出自:
C++异常处理
C++异常以及优缺点
C++异常详解
在规划这篇文章的时候,我给自己完成的目标是十几天,可没想到第三天就完成了,最大的不同是,原本我想把《Essential C++》这本书再读一遍,然后把里面晦涩难懂的地方挑出来,说给大家听,可没想到,后来我真的看不下去了(手动狗头),于是就按照目录,把我没接触过的,或者说之前没搞懂的知识点,通过网络的强大力量,过了一遍。这里我主要是有两点考虑,开学之后我还要研读《C++ Primer》,所以很多疑惑与不解,留到那个时候再说。其次,作为一个学生,把每一个知识点的每一个细节都学会,只能说异想天开(我是这么认为的),甚至我认为这是不可完成的任务。学那么暂时用不上,不如好好牢固最基础的知识。
这是我的第一篇博客,肯定会有很多很多的失误,如果大家耐心看到这里,并且发现了错误的话,请在评论区积极地和我对线(欢迎欢迎)。文章有一万多字,其中很多字都是借鉴别人的(奈何本人实力太弱)。但是第一篇博客就写一万字,这对我来说也是成就感颇深的一件事。这是第一篇,但不是最后一篇,希望我们在下一篇文章见!
完结撒花!!!
桂珠、张平、陈爱国 .Java面向对象程序设计(jdk1.6)第三版:北京邮电大学出版社,2005 ↩︎