C++ 菜鸟教程梳理

很久没写C++,决定过一遍runoob,只记录认为需要记录或个人印象不清晰的内容,唤醒一下沉睡的记忆

标准库

核心语言构建块、C++标准库、STL构成

基本语法

三字符组

用于表示键盘上没有的字符,如??=来表示#,大部分编译器都不会自动替换三字符组,但g++默认支持三字符组,当程序中需要两个连续的问号时,需要写成 "...?""?..."或转义为"...?\?..."

数据类型

1 字节(byte) 8 位(bit),
char 占用 1 字节,int 占用 4 字节,long 占用 4 字节,long long 占用 8 字节,float 占用 4 字节,double 占用 8 字节
以上类型的范围只是 C++ 标准规定的最小要求,实际上,许多系统上这些类型可能占用更多的字节,例如,很多现代计算机上 int 通常占用 4 字节,而 long 可能占用 8 字节。

float a;
double b;
a = 77777.7777777;
b = 77777.777777777;

打印出a和b后显示a为77777.781250,b为77777.777778,float 是 32 位浮点数,有 7 位有效数字。double 是 64 位浮点数,有 16 位有效数字。一般编译器规定小数后面最多保留6位数,但这个1250是怎么来的呢?

十进制数 77777.7777777 转换为32位二进制浮点数,在四舍五入后再转换回十进制时,得到了77777.781250

typedef 声明

为一个已有的类型取一个新的名字

typedef type newname; 

类型转换

包含四种类型转换:静态转换、动态转换、常量转换、重新解释转换

静态转换

通常用于较相似对象间的转换,不进行任何类型检查

int i = 10;
float f = static_cast<float>(i);

动态转换

通常用于将一个基类指针或引用转换为派生类指针或引用,在运行时进行类型检查

class Base {};
class Derived : public Base {};
Base* ptr_base = new Derived;
Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针转换为派生类指针

常量转换

用于将 const 类型的对象转换为非 const 类型的对象,只操作 const 属性,不会改变对象类型

const int i = 10;
int& r = const_cast<int&>(i);

重新解释转换

将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换,不进行任何类型检查。静态转换无法处理没有关联性的指针类型转换,也不能在多态类型之间实现转换,这时候就需要重新解释转换出手了。

// 将int类型转换为float类型
int i = 10;
float f = reinterpret_cast<float&>(i);
// 将整数指针转换为字符指针
int* intPtr = new int(42);
char* charPtr = reinterpret_cast<char*>(intPtr);

C++常量

注意把常量定义为大写字母,要有良好的编程习惯

cont int LENGTH = 10;

不应该把布尔值 true 和 false 看成 1 和 0 ,应只看成 真 和 假

C++修饰符

有符号整数和无符号整数修饰符有很大的差别,而不仅仅是体现在符号上。

#include <iostream>
using namespace std;

int main()
{
   short int i;
   short unsigned int j;
   j = 50000;
   i = j;
   cout << i << " " << j;
   return 0;
}

当上面的程序运行时,会输出下列结果:
-15536 50000
对于 unsigned short 类型,所有的位都被解释为数值位。因此,二进制 1100001101010000 直接被解释为50000。对于 short 类型,有符号整数使用二进制补码表示法,其中最高位(左)是符号位。所以最终解释为-15536

C++类型限定符、存储类

类型限定符/存储类定义
const常量,不能被修改
volatile该变量的值可能被当前程序以外的因素改变
restrict用于限定和约束指针,并表明该指针是访问这块内存数据对象的唯一且初始的方式
mutable类中的成员变量可以在 const 成员函数中被修改
static静态变量,该变量的作用域仅限于当前文件或当前函数内,不会被其他文件或函数访问
register寄存器变量,表示该变量被频繁使用,可以存储在CPU的寄存器中,以提高程序的运行效率
extern相当于提供一个全局变量的引用,可以理解为在另一个文件中声明一个全局变量或函数
thread_local线程本地存储变量,该变量仅可在它在其上创建的线程上访问,避免数据竞争和同步问题,在创建线程时创建,并在销毁线程时销毁

C++运算符

算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、杂项运算符

杂项运算符描述
sizeof返回变量字节数
A ? X:Y条件运算
,逗号运算符,按顺序求解表达式,整个逗号表达式的值是最后一个表达式的值,其余值被丢弃
.和->成员运算符,用于引用类、结构和共用体的成员
Cast强转
&和*指针运算符,返回变量地址和指向一个变量

C++函数

Lambda函数与表达式

Lambda 表达式把函数看作对象。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。具体形式如下:

[capture](parameters)->return-type{body}

其中:

  • capture:捕获值,用于指定Lambda表达式可以访问的外部变量,以及是按值还是按引用的方式访问。
  • parameter:参数列表。
  • return type:返回值类型。
  • function body:函数体

捕获方式

  • 值捕获:在捕获列表中使用变量名,表示将该变量的值拷贝到 Lambda 表达式中。值捕获的变量默认不能在 Lambda 表达式中修改,除非使用mutable关键字。
int x = 10;
auto f = [x] (int y) -> int { return x + y; }; // 值捕获 x
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 15,不受外部 x 的影响
  • 引用捕获:在捕获列表中使用&加变量名,表示将该变量的引用传递到 Lambda 表达式中,作为一个数据成员。引用捕获的变量在 Lambda 表达式调用时才确定,会随着外部变量的变化而变化。引用捕获的变量可以在 Lambda 表达式中修改,但要注意生命周期的问题,避免悬空引用的出现。
int x = 10;
auto f = [&x] (int y) -> int { return x + y; }; // 引用捕获 x
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 25,受外部 x 的影响
  • 隐式捕获:在捕获列表中使用=&,表示按值或按引用捕获 Lambda 表达式中使用的所有外部变量。这种方式可以简化捕获列表的书写,避免过长或遗漏。隐式捕获可以和显式捕获混合使用,但不能和同类型的显式捕获一起使用。
int x = 10;
int y = 20;
auto f = [=, &y] (int z) -> int { return x + y + z; }; // 隐式按值捕获 x,显式按引用捕获 y
x = 30; // 修改外部的 x
y = 40; // 修改外部的 y
cout << f(5) << endl; // 输出 55,不受外部 x 的影响,受外部 y 的影响
  • 初始化捕获:使用初始化表达式,从而在捕获列表中创建并初始化一个新的变量,而不是捕获一个已存在的变量。这个初始化变量在Lambda表达式创建时就会确定。可以捕获this指针。
int x = 10;
auto f = [z = x + 5] (int y) -> int { return z + y; }; // 初始化捕获 z,相当于值捕获 x + 5
x = 20; // 修改外部的 x
cout << f(5) << endl; // 输出 20,不受外部 x 的影响
class Test
{
public:
    Test(int n) : num(n) {} // 构造函数,初始化 num
    void add(int x) // 成员函数,增加 num
    {
        auto f = [*this] () { return num + x; };
        cout << f() << endl;
    }
private:
    int num; // 成员变量,存储一个整数
};

对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入.

C++ 数字

生成随机数

rand()只返回一个伪随机数。生成随机数之前必须先调用srand()函数。不然运行多次时每次生成的随机数序列都是相同的。

C++ 数组

传递数组给函数

三种方式

void myFunction(int *param){}
void myFunction(int param[10]){}
void myFunction(int param[]){}

数组类型会自动转换为指针类型,其实都是传的地址。

从函数返回数组

C++不允许返回数组,在这种情况下可以返回一个指针。但不能简单地返回指向局部数组的指针,因为当函数结束时,局部数组将被销毁,所以需要将其定义为static变量:

int* myFunction()
{
   static int myArray[3] = {1, 2, 3};
   return myArray;
}

动态分配new出来的数组就不用static了,不过要记得delete[]

C++ 指针

指针的算数运算

ptr++

执行 ptr++ 后,指针 ptr 会向前移动 n 个字节,指向下一个元素的地址。这是由于指针算术运算会根据指针的类型和大小来决定移动的距离。如整数占据 4 个字节, ptr++ 会将指针 ptr 向前移动 4 个字节,指向下一个整型元素的地址。如字符指针占据 1 个字节,因此 ptr++ 会将 ptr 的值增加 1,执行后 ptr 指向地址 1001。

指针与指针之间的减法运算:得到两个指针之间的元素个数。

C++ 日期和时间

在 C++ 程序中引用ctime头文件
有四个与时间相关的类型:clock_ttime_tsize_ttm。类型 clock_tsize_ttime_t 能够把系统时间和日期表示为某种整数。结构类型 tm 把日期和时间以 C 结构的形式保存。

当前日期和时间

#include <iostream>
#include <ctime>
using namespace std;
 
int main( )
{
   // 基于当前系统的当前日期/时间
   time_t now = time(0);
   // 把 now 转换为字符串形式
   char* dt = ctime(&now);
}

C++ 基本输入输出

  • 标准输出流cout
  • 标准输人流cin
  • 标准错误流cerr
  • 标准日志流clog

cerr不经过缓冲直接输出,能够比cout更迅速输出错误信息,在紧急情况下还能得到输出功能的支持。所以尽量使用 cerr 流来显示错误消息,而其他的日志消息则使用 clog 流来输出。

C++ 结构体

struct语句

struct type_name {
member_type1 member_name1;
member_type2 member_name2;
member_type3 member_name3;
...
} object_names;  // object_names是顺便声明的实例

成员访问运算符.
指针访问运算符->

struct Books
{
	...
}Book1;
Book1.book_id = 12345;

struct Books* struct_pointer;
struct_pointer = &Book1;
struct_pointer->title;

typedef 关键字

typedef struct student{
    char name[20];int num;
    float math,yuwen,wuli,mean;
} st;
st st1, st2, st3;

其实就是结合 typedef,方便后续变量的定义。

C++ 类 & 对象

类成员函数

使用范围解析运算符 :: 来定义

类访问修饰符

标记公有成员、受保护成员、私有成员
实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数

构造函数、析构函数

构造函数可用于为某些成员变量设置初始值。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

构造函数使用初始化列表来初始化字段

Line::Line( double len): length(len)
{
    cout << "Object is being created, length = " << len << endl;
}
// 相当于
Line::Line( double len)
{
    length = len;
    cout << "Object is being created, length = " << len << endl;
}

拷贝构造函数

是一种特殊的构造函数,在创建对象时使用同一类中之前创建的对象来初始化新创建的对象。如果不写拷贝构造函数,C++只会简单的复制上一个对象的指针,而并非真正构造了新的对象,即只实现浅拷贝。相当于多配了一把钥匙,而非新盖了一栋楼。

使用拷贝构造函数的三种情况:

  • 当用一个对象去初始化同类的另一个对象时
  • 当某函数的一个参数是类A的对象,那么该函数被调用时,类A的拷贝构造函数将被调用
  • 如果函数的返回值是类A的对象,则返回时A的拷贝构造函数被调用
    拷贝构造函数实现的是深拷贝。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。

类的拷贝构造函数原型通常是这样:

类名(const 类名& 原对象)

拷贝构造函数的几个特点:
1、函数名和类名相同,因为它也是构造函数的一种;
2、第一个参数必须是一个自身类类型的引用,且其他参数都有默认值。因为如果不是引用就需要拷贝实参(原对象),但拷贝实参又需要调用拷贝构造函数(打不过钢龙怎么办?那你穿钢龙套啊!)

友元函数

类的友元函数通常申明在类内部(具体public,protected,private无所谓),定义在类外部,但有权访问类的private成员和protected成员。友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

  • 使用关键字friend
class Box
{
   double width;
public:
   double length;
   friend void printWidth( Box box );
};

内联函数

如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方,这样不用再调用函数,提高函数的执行效率。但如果对内联函数进行任何修改,都需要重新编译函数的所有客户端。通常是函数体较小又调用频繁的函数。

  • 使用关键字inline

C++ 的 this 指针

在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址,它指向当前对象的实例。

#include <iostream>
 
using namespace std;
 
class Box
{
   public:
      // 构造函数定义
      Box(double l=2.0, double b=2.0, double h=2.0)
      {
         cout <<"调用构造函数。" << endl;
         length = l;
         breadth = b;
         height = h;
      }
      double Volume()
      {
         return length * breadth * height;
      }
      int compare(Box box)
      {
         return this->Volume() > box.Volume();
      }
   private:
      double length;     // 宽度
      double breadth;    // 长度
      double height;     // 高度
};
 
int main(void)
{
   Box Box1(3.3, 1.2, 1.5);    // 声明 box1
   Box Box2(8.5, 6.0, 2.0);    // 声明 box2
 
   if(Box1.compare(Box2))
   {
      cout << "Box2 的体积比 Box1 小" <<endl;
   }
   else
   {
      cout << "Box2 的体积大于或等于 Box1" <<endl;
   }
   return 0;
}

指向类的指针

  • 与指向结构的指针类似,需要使用成员访问运算符 ->
  • 可用于动态分配内存,创建类对象MyClass *ptr = new MyClass;

类的静态成员

可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。

class Box
{
   public:
      static int objectCount;
      Box(...)
      {
         ...
      }
      ...
   private:
	...
};
 
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
 
int main(void)
{
   ...;
   return 0;
}

静态成员函数

函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据其他静态成员函数类外部的其他函数

C++ 继承

// 基类
class Animal {
    ...
};
class Dog {
	...
};
//派生类
class Labrador : public Animal, public Dog {
    ...
};

访问控制和继承

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

继承类型

几乎不使用 protectedprivate 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

重载运算符和重载函数

参数列表和定义(实现)不同,编译器通过比较参数类型选用最适合的定义。

运算符重载

重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

返回值类型 operator重载的运算符(参数列表)
{
	...
}

类内重载时,重载函数作为类的成员函数,可以只传入一个参数(另一个参数由this运算符进行访问);类外重载时,用友元函数的方法实现(在类内声明为友元函数,在类外定义)

#include <iostream>
using namespace std;
 
class Box
{
   public:
      ...
      // 重载 + 运算符,用于把两个 Box 对象相加
      Box operator+(const Box& b)
      {
         Box box;
         box.length = this->length + b.length;
         box.breadth = this->breadth + b.breadth;
         box.height = this->height + b.height;
         return box;
      }
   private:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
};
// 程序的主函数
int main( )
{
   Box Box1;                // 声明 Box1,类型为 Box
   Box Box2;                // 声明 Box2,类型为 Box
   Box Box3;                // 声明 Box3,类型为 Box
   ...
   Box3 = Box1 + Box2;
   ...
}
  • 注意:返回值当左值得时候,返回的是一个引用

C++ 多态

基类中的函数在子类中有其分别的独立实现。
如果想执行子类中的实现,需要在基类中使用关键字virtual进行声明,将其定为虚函数。如果没有virtual声明,无论执行基类方法还是子类方法,函数会被调用为基类中的版本,这就是所谓的静态多态,或静态链接。
在基类中不能对虚函数给出有意义的实现,用到纯虚函数virtual ... = 0

class Shape {
   public:
      Shape( int a=0, int b=0)
      {
         ...;
      }
      // pure virtual function
      virtual int area() = 0;
};

= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。

C++ 数据抽象

只向外界提供关键信息(接口),隐藏其后台的实现细节。

C++数据封装

通过将数据和操作数据的函数封装在一个类中来实现。确保了数据的私有性和完整性。

C++ 接口(抽象类)

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。C++ 接口是使用抽象类来实现的。
设计抽象类(ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。

C++ 文件和流

文件流标准库fstream定义了三种新的数据结构:

数据类型描述
ofstream表示输出文件流,用于创建文件并向文件写入信息
ifstream表示输入文件流,用于从文件读取信息
fstream表示文件流,且同时具有 ofstream 和 ifstream 两种功能

C++ 异常处理

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
try
{
   // 保护代码
}catch( ExceptionName e1 )
{
   // catch ExceptionName e1 类型的异常
}catch( ExceptionName e2 )
{
   // catch ExceptionName e2 类型的异常
}catch( ... )
{
   // 省略号表示 catch 任何类型的异常
}

ExceptionName是异常声明,用于指定捕捉的异常类型
也可以定义新的异常:

#include <iostream>
#include <exception>
using namespace std;
 
struct MyException : public exception
{
  const char * what () const throw ()
  {
    return "C++ Exception";
  }
};
 
int main()
{
  try
  {
    throw MyException();
  }
  catch(MyException& e)
  {
    std::cout << "MyException caught" << std::endl;
    std::cout << e.what() << std::endl;
  }
  catch(std::exception& e)
  {
    //其他的错误
  }
}

what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。

预处理器

条件编译

有选择地对部分程序源代码进行编译

#ifdef DEBUG
	cerr << "Variable x = " << x << endl;
#endif
#if 0
	不进行编译的代码
#endif

等等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值