Bootstrap

《Essential C++》学习笔记

《Essential C++》这本书,是适合从C选手快速过度到C++选手的 一本书,下面是个人记录

第一章:基础语法

第一章主要就是C语言基础,这里类似于表达式 数组 条件语句 循环语句,就不多概述了。

:::info
vector:可动态扩增的Array

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

:::

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

  1. 指针和引用的区别:
    • 在本质上来说 指针和引用都是变量 存放的都是被引用对象的地址
    • 指针变量本身可以被寻址
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 指针”得到的是指针本身(所指向的变量或对象的地址)的大小
- <font style="color:#DF2A3F;">当我们对指针进行解引用操作时(*p),一定要确定其值并非0,对于引用来说,因为它一定会代表某个对象,所以不需要做这样的检查</font>
  1. 堆内存
    1. 堆允许程序在运行时动态地申请某个大小的内存空间。
    2. 在C++中,通过new出来的对象,需要使用delete加以释放。
  2. inline函数:用来替代C语言的宏定义
    1. inline函数可以解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题
#include <stdio.h>
//函数定义为inline即:内联函数
inline char* f(int i) {
    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);这样就提高了运行效率
*/

:::info

  • 也就是说,在函数体很小的情况下,使用Inline函数可以提高效率
  • 需要注意的一点是,将函数定义为inline,仅仅是对编译器的一种建议,编译器是否执行这个请求,需视编译器而定。
  • ⚠️⚠️⚠️
  • inline函数虽好,但是需要慎用
    • 内联是以代码膨胀复制为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码, 将使程序的总代码量增大,消耗更多的内存空间。
    • 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
    • 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

:::

第三章:泛型编程风格

STL部分

这部分和Java以及Kotlin里面的容器几乎差不多

  1. 顺序型容器 vector与list
    1. vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。
    2. 查找效率很高,为 O(1)。但是插入和删除就比较麻烦。
    3. 而vector和java里的ArrayList一样,都具有自动扩容的功能。
    4. 不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。
    5. list底层由双向链表实现,因此内存空间是不连续的。所以插入时间复杂度低,查找时间复杂度高。
  2. 关联容器 map和set
    1. java,里面也有类似的数据结构,map一般表示一对对的key/value组合,俗称键值对。而set就只含有key。
    2. set不允许键值重复,可以用此解决一些算法问题,例如环形链表问题。
    3. map也不允许键值重复,但是map允许修改键对应的值。set则不能修改键值。
    4. set和map底层都是红黑树,红黑树是一种具有自动平衡功能的二叉树,在set中,如果想要修改键值,那么就会破坏红黑树的结构, 所以STL中将set的迭代器设置成const,不允许修改迭代器的值。
  3. queue和stack这个和Java里面的双向链表很像,就不多说了。

Iterator(迭代器)

迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。从这一点上看,迭代器和指针类似。

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

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指针

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;
}

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

Static关键字

该实例归所有对象共有。
static的作用(在C/C++)

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

static的特点

  • 静态变量都在全局数据区分配内存
  • 未经初始化的静态全局变量会被程序自动初始化为0
  • static除了修饰成员变量之外,还可以修饰函数,叫做静态成员函数。普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。
  • 被 static 修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要 new 出一个类来
  • 被 static 修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要 new 出一个类来
  • 在 C++ 中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。
  • static除了修饰成员变量之外,还可以修饰函数,叫做静态成员函数。普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。
#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;
}

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

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

多态:允许将子类类型的指针赋值给父类类型的指针。和java类似这样实现了一个函数会根据传入参数的不同有不同的功能。

在程序编译阶段,对象还没有产生时,程序只能根据指针的类型来判断调用哪个函数,这时候都是调用的父类函数,当子类重写父类方法时 同时也是把继承自父类的虚表中对应函数的索引的函数指针从父类函数改成了自己的函数。这就造成了子类和父类对象里面的虚表内容不同。所以动态联编时 去子类 或者父类里面找虚表,调用的函数不同。这也就完成了多态。

#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(模板)进行编程

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

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


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

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

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

#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");
}
  • 模板可以显著减小源代码的大小并提高代码的灵活性,而不会降低类型安全。
  • 编译器由模板自动生成函数的过程叫模板的实例化。

例如:

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 &)函数

模板调用语句可以明确指明要把类型参数实例化为哪种类型。可以用:模板名<实际类型参数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;
}
// 此处实例化的模板函数原型应为:double In(double);

第七章:异常处理

异常的几个关键字和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 异常的代码
}

  • 异常的优点:
    • 函数的返回值可以忽略,但异常不可忽略。如果程序出现异常,但是没有被捕获,程序就会终止,这多少会促使程序员开发出来的程序更健壮一点。而如果使用C语言的error宏或者函数返回值,调用者都有可能忘记检查,从而没有对错误进行处理,结果造成程序莫名其面的终止或出现错误的结果。
    • 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
    • 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
    • 异常处理可以在调用跳级。这是一个代码编写时的问题:假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。
  • 异常需要的几个注意点:
    • 如果异常抛出是用的 int 类型 而在外部捕获中 捕获函数是用的char类型进行的捕获那么,该错误就不会被捕获到,而代码也就此终止异常继续往外传。
    • 性能问题。这个一般不会成为瓶颈,但是如果你编写的是高性能或者实时性要求比较强的软件,就需要考虑了。
    • 指针和动态分配导致的内存回收问题:在C++中,不会自动回收动态分配的内存,如果遇到异常就需要考虑是否正确的回收了内存。在java中,就基本不需要考虑这个。
    • 函数的异常抛出列表:java中是如果一个函数没有在异常抛出列表中显式指定要抛出的异常,就不允许抛出;可是在C++中是如果你没有在函数的异常抛出列表指定要抛出的异常,意味着你可以抛出任何异常。
    • C++中编译时不会检查函数的异常抛出列表。这意味着你在编写C++程序时,如果在函数中抛出了没有在异常抛出列表中声明的异常,编译时是不会报错的。
    • 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟 踪调试时以及分析程序时,比较困难。
;