《Essential C++》学习笔记
《Essential C++》这本教材专为希望从C语言学习者顺利过渡至C++编程领域的读者编写而成。以下是我在学习过程中所做的详细笔记。
第一章:基础语法
本章的主要内容是介绍C语言的基础知识。这里包含表达式、数组、条件语句以及循环语句等基本概念等。
:::info
vector:可动态扩增的Array
- Vector,该是一种功能多样的模板类与函数库,能够操作多种数据结构与算法. * vector,是一种存储任意类型动态数组的数据容器,既能动态地增加数据量又能够有效地压缩存储空间.
:::
第二章:面向过程的编程风格
- 指针和引用的区别:
- 从根本上讲,指针和引用都属于变量类型,并且它们都用于存储对象的内存地址。
- 指向器变量自身能够直接指向内存地址。
int a =100;
int *p= &a;//p存放的是a的地址
int **p1 = &p;//p1存放的就是p的地址
cpp
- 而引用变量地址却不可被寻址,假如引用变量为r,&r操作得到的只能是r所指向对象的地址,而不是r本身的地址。
- 数组元素允许是指针常量,而不能是引用例如 a作为一个引用数组 a[0]=1; 无法确定是a[0]的值为1还是a[0]所引用的值为1,容易产生二义性。
- 引用不能为空,而指针可以为空。你可以只声明一个指针变量,而不去给它赋值。一个未指向任何对象的指针,其地址值为0.
- 指针可以有多级,而引用只能一级
- "sizeof 引用"得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小
- <font style="color:#DF2A3F;">当我们对指针进行解引用操作时(*p),一定要确定其值并非0,对于引用来说,因为它一定会代表某个对象,所以不需要做这样的检查</font>
-
堆内存
-
堆允许程序在运行时动态地申请某个大小固定的内存空间。
-
在C++中, 由new生成的对象, 需要通过delete操作符进行释放。
-
内联函数用于代替C语言中的预编译指令
-
内联函数能够有效避免小型频繁调用的子函数严重占用栈空间或导致栈溢出。
#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);这样就提高了效率
*/
cpp

:::info
- 在小规模的功能体下,默认情况下启用内联编译器优化有助于提升效率。
- 需要特别注意的是,默认情况下启用内联是一种对编译器而言的优化建议,并非强制性的措施。
- ⚠️⚠️⚠️
- 内联功能虽好但也存在适用范围限制。
- 采用内联方式会伴随一定的性能代价仅减少了调用过程中的开销有助于提升运行效率。
- 然而当功能体较长时可能导致额外内存消耗。
- 如果出现循环结构则会增加额外计算负担。
:::
第三章:泛型编程风格
STL部分
这部分和Java以及Kotlin里面的容器几乎差不多
顺序型容器 vector与list
-
向量容器与数组在内存布局上具有相似性,并且起始地址固定。
-
查询操作的时间复杂度为 O(1) 。然而插入和删除操作的时间复杂度较高。
-
vector与其在 Java 中的 类具有类似的特性。
-
具体实现细节有所不同:Visual Studio 2015采用一倍半扩容策略;而 GCC则采用两倍扩容策略。
-
list 的底层基于双向链表的数据结构;因此其内存空间分布不连续。
-
关联容器 Map 与 Set
-
在 Java 中也存在类似的容器结构,其中 Map 通常表示键值对的一对一映射关系。而 Set 则仅包含键。
-
Set 不允许出现重复的键值组合,在处理一些特定场景时非常有用(例如解决环形链表问题等场景)。
-
Map 和 Set 都基于红黑树实现底层数据结构,在 Set 中无法修改任意键值的情况下,默认迭代器为 const 类型以防止数据变更。
-
Map 允许更新对应的键值而不影响整体结构,但在 Set 中由于底层结构限制无法进行此类操作。
-
queue和stack这个和Java里面的双向链表很像,就不多说了。
Iterator(迭代器)
一个迭代器可能指向容器中的任意指定元素,在这种情况下可以通过操作该迭代器即可访问并修改其对应的元素内容。从功能角度来看,在某种程度上与指针机制相似。
vector<int> v;//声明一个int类型的可变长数组
vector<int>::iterator i;//定义一个迭代器
for (i = v.begin(); i != v.end(); ++i) //用迭代器遍历容器
cout << i << " "; //*i 就是迭代器i指向的元素
cpp
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;
cpp

第四章:基于对象的编程风格
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;
}
cpp

struct Theshy //C代码
{
int price;
};
void SetNum(struct Theshy* this, int p) //不一样的地方
{
this->price = p;
}
int main()
{
struct Theshy shy;
SetNum(­, 20000);
return 0;
}
c

this指针是在成员函数中用来指向其调用者(一个对象)
Static关键字
该实例归所有对象共有。
static的作用(在C/C++)
- 在对某个变量进行静态修饰时,默认只会初始化一次以延长其局部生命周期。
- 例如,在函数体内存放数组且不希望其被释放时,则可使用static关键字进行静态修饰。
static的特点
- 全局数据区域分配了用于存储静态变量的空间。
- 未预先设置初始值的全局静态变量默认会被系统初始化为零值。
- static不仅用于修饰类中的非-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;
}
c

- 静态成员函数主要为了调用方便,不需要生成对象就能调用。
第五章:面对对象的编程风格
多态:允许将子类对象的指针赋值为父类对象的指针。类似于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;
}
cpp

在这种特定情况下,在运行该程序时会输出A的结果;这种现象属于静态联编的情况,在特定条件下会导致错误行为的发生。
第六章:以template(模板)进行编程
整章的内容都是标准模板。例如,在本章中我们需要创建一个函数来实现不同类型的数值比较大小的作用。
int max(int x,int y);
{return(x>y)?x:y ;}
float max( float x,float y){
return (x>y)? x:y ;}
cpp
模板充当代码重用机制中的工具,在其中扮演工具角色。它能够完成类型参数化的任务,并将类型设定为参数形式以确保代码的真正可重用性。
以下为实现一个求最小值函数模板
#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");
}
cpp

- 模板能够大幅缩减源代码的规模并增强其灵活性的同时不降低类型安全。
- 通过模板自动生成函数的过程称为函数的模板实现。
例如:
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 &)函数
cpp

在C++中使用模板调用语句时,默认情况下会将所有类型的成员函数传递给实例对象;如果需要指定不同的行为,则需要通过重载函数或使用别名的方式进行关联。
#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);
cpp

第七章:异常处理
与Java中的几个关键异常现象紧密相关的几个关键字都绕不开。
- 抛出异常代码
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";//用到了throw语句
}
return (a/b);
}
cpp
- 捕获异常代码
try
{
// 保护代码
}catch( ExceptionName e )
{
// 处理 ExceptionName 异常的代码
}
cpp
- 异常的优点:
虽然函数返回值可被忽视但异常却承载了重要信息。未被捕获的异常会导致程序直接终止 一定程度上能促使开发者编写出更可靠的程序。 - 而如果仅依赖C语言中的error机制或基于返回值的设计 那么调用者往往忽视潜在问题 这就可能导致非预期终止或出现无法预测的结果。
- 整型返回值缺乏语义信息 而通过类名就能获取到相关信息。
- 异常作为一个特殊的类型不仅可以传递类型信息还可以携带额外的状态描述 这使得它们更适合于复杂场景下的错误管理。
- 异常处理能够跳过层级限制 这是一个代码编写时的关键问题:当在一个复杂的函数调用链中出现问题时 使用整型返回码的方式需要让每一个参与处理的任务都去检查并响应 这种做法不仅效率低下而且容易导致代码冗余。相反 如果采用基于栈展开式的异常机制 那么只需在一个关键点处进行处理 就能自动覆盖整个错误传播路径 这大大简化了错误调试的工作流程并提高了系统的容错能力。
编写程序时需要注意的几个要点:
当异常使用int类型时,在外部捕获函数中使用char类型进行捕获的情况,则该错误不会被捕获到,并导致程序在此处终止,并将异常传递给外部系统。
性能问题通常不会成为主要瓶颈。
对于需要高性能或实时性较高的软件开发项目,则需要特别注意。
C++语言中没有自动回收动态分配的内存空间。
相比之下,在Java语言中就无需考虑这一问题。
指针操作及动态内存分配可能会引发内存回收相关的问题。
如果一个函数在其声明中没有明确列出要抛出的所有可能的异常类型,则无法通过编译器对这种潜在的问题发出警告。
相反,在C++语言中则不需要面对这一限制。
编写代码时出现任何此类错误都会使调试过程变得复杂,
尤其是在调试大型系统代码时,
这个问题的影响会更加显著
