C++学习笔记

摘要

本文参考了清华大学出版社的本科教材《C++语言程序设计(第五版)》同时结合互联网资料,记录了学习C++的重难点,并舍弃了一些过于基础的内容,比如选择语句、循环语句等。本文尽可能详尽记录C++的语法、原理等内容,可用作学习后期查缺补漏。

第一章 C++基础

一、基本数据类型

C++基本数据类型包括bool、char、int、float、double。

二、运算符

1.位运算符

(1)按位与 &(2)按位或 |(3)按位异或 ^(4)按位取反 ~(5)移位

2.运算符优先级

运算符优先级
优先级运算符综合性
1[],(),.,->,后置++,后置--左→右
2前置++,前置--,sizeof,&,*,+(正号),-(负号),~,!右→左
3(强制转换类型)右→左
4.*,->*左→右
5*,/,%左→右
6+,-左→右
7<<,>>左→右
8<,>,<=,>=左→右
9==,!=左→右
10&左→右
11^左→右
12|左→右
13&&左→右
14||左→右
15?右→左
16=,*=,/=,%=,+=,-=,<<=,>>=,&=,^=,|=右→左
17,左→右

综合性是指当一行代码中出现多个同级别的符号时的优先级。

3.混合运算时数据类型的转换

(1)隐含转换

将低类型数据转换为高类型数据,这种转换是安全的,因为在转换过程中数据的京都没有 损失。

char(unsigned) ->  short(unsigned) ->  int(unsigned)  ->  long(unsigned)  ->  long long  -> 

低精度 -----------------------------------------------------------------------------------------------------------

float  -> double 

------>高精度

(2)显式转换

float a = 1.0;

类型说明符 (表达式)   // C++ 风格
int b = int(a);

(类型说明符) 表达式   // C语言风格
int c = (int)a;

使用隐式转换需要注意的是:(1)可能会影响精度(2)转换是一次性的,不会影响原数据

三、goto语句

使用goto语句可能会导致程序混乱,但在多重循环中执行跳出循环很有效。

四、类型别名与类型推断

1.类型别名

除了可以使用内置的基本数据类型名和自定义的数据类型名以外,还可以为一个已有的数据类型另外命名。

typedef 已有类型名 新类型名表;
typedef double Area, Volume;    // 可以为一个已有数据类型声明多个别名

另外,还可以使用别名声明来定义一个类型别名。

using 新类型名 = 已有类型名;
typedef double Area, Volume;
需要改写成:
using Area=double;
using Volume=double;

2.auto类型与decltype类型

auto类型用于推断一个表达式所产生的类型。例如:

auto val=val1+va2;

auto与其他类型一样可以定义多个变量:

auto i=0,j=1;          // 正确示范
auto size=0, pi=3.14;  // 错误示范

在某些情况下,我们定义一个变量与某一表达式的类型相同,但并不想用该表达式初始化这个变量,这时需要decltype变量,它的作用是选择并返回操作数的数据类型。

decltype(i) j = 2;

上述声明表示j以2作为初始值,类型与i一致。

五、变量的实现机制

C++变量的值都存放在内存中,内存中的每个单元都有一个唯一的编号,这个编号就是它的地址,不同内存单元的地址互不相同。

任何类型的数据,在内存中都是用二进制的形式存储的,一串二进制数,只有与适当的数据类型关联后,才有真实的含义。

C++语言表达式的执行原理:C++大部分读写都是对寄存器进行的。

六、函数

1.函数调用

如果希望在定义一个函数前调用一个函数,则需要在调用前声明函数。

类型说明符 函数名(含类型说明的形参列表);

(1)递归调用

函数可以直接或间接的调用自身。

(2)嵌套调用

2.参数传递

在函数未被调用时,函数的形参并不占有实际的内存空间,也没有实际的值。只有在函数调用时才为形参分配存储单元。

(1)值传递

值传递是指发生函数调用时,给形参分配内存空间,并用实参来初始化形参。这一过程是参数值的单向传递过程,一旦形参获得了值便与实参脱离关系,此后无论形参发生了怎么样的改变,都不会影响实参。

(2)引用传递

用引用作为形参,在函数调用时发生的参数传递,称为引用传递。

引用是一种特殊的类型的变量,可以被认为是另一个变量的别名,通过引用名与通过被引用的变量名访问变量的效果是一样的,例如:

int i,j;
int &ri=i;
j = 10;
ti = j;  //相当于i=j

使用引用时必须注意下列问题。

  • 声明引用时,必须对其初始化,使其指向一个已存在的对象。
  • 一旦一个引用被初始化后,就不能改为指向其他对象。 

也就是说,一个引用,从它诞生之时起,就必须确定是哪个变量的别名,而且始终只能作为一个变量的别名,不能另作他用。

(3)可变数量形参

3.内联函数

对一些规模较小、功能简单又使用频繁的函数,可设计为内联函数。内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。

inline 类型说明符 函数名(含类型说明的形参表)
{
    语句序列
}

inline关键字只是表示一个要求,编译器并不承诺将inline修饰的函数作为内联函数。而在现代编译器中,没有用inline修饰的函数也可能被编译为内联函数。通常内联函数应该是比较简单的函数。如果函数过于复杂,多数编译器会将其自动转换为普通函数来处理。

#include <iostream>
using namespace std;

const double PI = 3.14159265358979;

// 内联函数,根据圆的半径计算其面积
inline double calArea(double radius){
    return PI * raduis * raduis;
}

int main(){
    double r = 3.0;
    // 调用内联函数求圆的面积,编译时此处被替换为CalArea函数体语句,
    // 展开为area=PI * radius * radius;
    double area = calArea(r);
    cout<<area<<endl;
    return 0;
}



运行结果为:
28.2743

4.constexpr函数

constexpr函数是指能用于常量表达式的函数。定义时需要遵守几项约定:函数的返回类型以及所有的形参类型都必须是常量,而函数体中必须有一条return语句:

constexpr int get_size() {return 20;}
constexpr int foo = get_size();
// 如果arg是常量表达式,则len(arg)也是常量表达式
constexpr int len(int arg){return get_size() * arg;}

5.带默认形参的函数

函数在定义时可以预先声明默认的形参值。

int add(int x=5, int y=6)    // 声明默认形参值
{
    return x+y;
}

int main()
{
    add(10, 20);            // 用实参来初始化形参,实现10+20
    add(10);                // 形参x采用实参值10,y采用默认值6,实现10+6
    add();                  // x和y采用默认值,分别为5和6,实现5+6
}

有默认值的形参必须在形参列表的最后

int add(int x, int =5, int z=6);    // 正确
int add(int x=1, int y=5, int z);     // 错误
int add(int x=1, int y, int z=6);    // 错误

在相同的作用域内,不允许在同一个函数的多个声明中对同一个参数的默认值重复定义,即使前后定义的值相同也不行。

int add(int x=5, int y=6);    // 默认形参值在函数原型中给出

int main()
{
add();
return 0;
}

int add(int x, int y)            //这里不能再出现默认形参,但为了清晰,可以通过注释说明默认形参
{
    return x+y;
}

6.函数重载

两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数,这就是函数的重载。

重载函数的形参必须不同:个数不同或者类型不同。如果函数名相同,形参类型也相同(论文函数返回值类型是否相同),在编译时会被认为是语法错误(函数重复定义)。

// 形参类型不同
int add(int x, int y);
float add(float z, float y);

// 形参个数不同
int add(int x, int y);
int add(int x, int y, int z);

// 错误,编译器不以形参名来区分函数
int add(int x, int y);
int add(int a, int b);

// 错误,编译器不以返回值来区分函数
int add(int x, int y);
void add(int x, int y);

在使用函数重载时候要注意不能将不同功能的函数定义为重载函数,也要防止二义性。

// 功能不同
int add(int x, int y){return x+y;}
float add(float x, float y){return x-y;}

// 防止出现二义性
void fun(int len, int w=2, int h=3);
void fun(int len);
// 调用
fun(1);

7.C++内置函数

C++参考手册http://www.cppreference.com

 8.运行栈工作原理

局部变量遵守“后进先出”原则。在一组嵌套调用的中,越早开始的调用,返回的越晚。这种栈叫做运行栈。运行栈中的数据分为一个一个的帧栈,每个帧栈对应一次函数的调用,帧栈中包括这次函数调用中的形参值、一些控制信息、局部变量值和一些临时数据。虽然一个函数在被调用时的形参和局部变量地址是不确定的,但他们的地址相对于栈顶地址却是确定的,这样就可以通过栈顶的地址,定位形参和局部变量。

第二章 类与对象 

 一、面向对象

1.封装

class Clock                                      
{
public:   
    void setTime(int newH, int newM, int newS);
    void showTime();
private:
    int hour, minute, second;     
};

2.继承

C++拥有多继承。

3.多态

强制多态、重载多态、类型参数化多态、包含多态。

4.类和对象

class Clock{
public:
    // 外部接口
protected:
    // 保护型成员
private:
    // 私有成员
};

在类中可以只声明函数的原型,函数的实现(即函数体)可以在类外定义。

公有属性、私有属性、保护属性

图 ​​​​​类成员访问控制属性

 声明对象:

类名 对象名;

Clock myClock;

 调用对象:

对象名.数据成员名
对象名.函数成员名(参数表)
myClock.showTime();

5.类的成员函数

按照上文代码中类的定义,函数的原型声明要写在类体中,原型说明了函数的参数表和返回值类型。而函数的具体实现是写在类定义外的。与普通函数不同的是,实现成员函数时要指明类的名称,具体的形式为:

返回值类型 类名::函数成员名(参数表){
    函数体
}

void Clock::setTime(int newH, int newM, int newS){
    hour=newH;
    minute=newM;
    second=newS;
}

void Clock::showTime(){
    cout<<hour<<":"<<minute<<":"<<second<<endl;
}

6.带默认形参值的成员函数

class Clock{
public:
    void setTime(int newH=0, int newM=0, int newS=0);
    ...
};

7.内联成员函数

内联函数的声明有两种方式:隐式声明和显式声明。

将函数体直接放在类体内,这种方法称之为隐式声明。可以使用关键字inline显式声明的方式。

inline void Clock::showTime(){
cout<<hour<<":"<<minute<<":"<<second<<endl;
}

8.构造函数和析构函数

(1)构造函数

构造函数在对象被创建的时候将被自动调用。

委托构造函数:

Clock(int newH, int newM, int newS)        // 第一个构造函数
{
    hour=newH;
    minute=newM;
    second=newS;
}

Clock():Clock(0, 0, 0){}                    // 第二个构造函数

第二个构造函数委托给了第一个构造函数来完成数据成员的初始化。当一个构造函数委托给另一个构造函数时 ,受委托的构造函数的初试值列表和函数体依次执行,然后控制权才会还给委托者函数。

复制构造函数:

其形参是本类的对象的引用。其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新的对象。

class Ponint{
public:
    Ponint(int xx=0, int yy=0){
        x=xx;
        y=yy;
    }
    Point(Point &p);
    int getX(){return x;}
    int getY(){return y;}
private:
    int x, y;
};


Point::Point(Point &p)
{
    x=p.x;
    y=p.y;
    cout<<"Calling the copy constructor"endl;
}

void f(Point p)
{
    cout<<p.getX()<<endl;
}

Point g()
{
    Point a(1, 2);
    return a;
}

int main(){
    // 当用类的一个对象去初始化该类的另一个对象时
    Point a(1, 2);
    Point b(a);
    Point c = a;    // 另一种写法
    cout<<b.getX()<<endl;
    // 如果函数的形参是类的对象,调用函数时,进行形参和实参结合时
    Point a(1, 2)
    f(a);
    // 如果函数的返回值是类的对象,函数执行完成返回调用者时
    Point b;
    b = g();
    return 0;
}

析构函数:

用来完成对象被删除前的一些清理工作,析构函数是在对象的生存周期即将结束的时刻被自动调用的。析构函数不会接受任何参数。

~Clock(){}

移动构造函数:

复制构造函数通过复制的方式构造新的对象,而很多时候被复制的对象仅作复制之用后销毁,在这时,如果使用移动已有的对象而非复制对象将大大提高性能。

左值(locator value):通俗来讲就是可以使用&符号取到地址的。

右值(read value):左值之外就是右值。

class MyStr{
public:
    string s;
    MyStr():s(""){};                                       // 无参构造函数
    MyStr(string_s):s(std::move(_s)){};                    // 有参构造函数
    MyStr(MyStr &&str) noexcept : s(std::move(str.s)){}    // 移动构造函数
};

default、delete函数

二、组合

类的组合描述的就是一个类内嵌其他类的对象作为成员的情况,它们之间的关系是一种包含与被包含的关系 。

当创建对象时,如果这个类具有内嵌对象成员,那么各个内嵌对象将被自动创建。在创建对象时既要对本类的基本对象数据成员进行初始化,又要对内嵌对象进行初始化。

类名::类名(形参表):内嵌对象1(形参表), 内嵌对象2(形参表), ...
{类的初始化}


Circle::Circle(float r):radius(r){}    // 实际上这里的radius是float radius创建的对象,并对其赋值

 将会按照调用内嵌函数的构造方法->执行本类构造函数的函数体的顺序调用。而析构函数正好相反。

前向引用声明

三 、结构体和联合体

1.结构体

结构体就是一种特殊形态的类。结构体和类的唯一区别在于,结构体和类具有不同的默认访问控制属性:在类中,对于未指定访问控制属性的成员,其访问控制属性为私有类型(private);在结构体中,对于未指定任何访问控制属性的成员,其访问控制属性为公有属性(public)。

struct 结构体名称
{
    公有成员
protected:
    保护成员
private:
    私有成员
};

实际上,引入结构体是为了保持和C程序的兼容性。在写C++程序时完全可以不使用结构体。结构体的变量可以使用下面的语法形式赋值,类也可以使用本方式赋值:

类型名 变量名 = {成员数据1初值, 成员数据2初值, ...};
struct Student{
    int name;
    string name;
    char sex;
    int age;
}


int main(){
    Student stu = {2023, "CRUD", "F", 23};
    cout<<stu.name<<endl;
}

2.联合体

例如,如果需要存储一个学生的各门课程的成绩,有些课程的成绩是同等级制的,需要用一个字符来存储它的等级,有些课程只记“通过”和“不通过”,需要用一个布尔值来表示是否通过,而另一些课程的成绩是百分制的,需要用一个整数来存储它的分数,这个分数就可以用一个联合体表示。

联合体是一种特殊形态的类,它可以有自己的数据成员和函数成员,可以有自己的构造函数和析构函数,可以控制访问权限,与结构体一样,联合体也是从C语言继承而来的。

联合体的全部数据成员共享同一组内存单元。

union 联合体名称
{
    公有成员
protected:
    保护成员
private:
    私有成员
};

例如,成绩这个联合体可以声明如下:

union Mark{
    char grade;
    bool pass;
    int percent;
}

由于联合体的特性,以上最多只能有一个成员属性有意义。另外,联合体有下面一些限制。

  1. 联合体的各个对象成员,不能有自定义的构造函数、自定义的析构函数和重载的赋值运算符,不仅联合体的对象成员不能有这些函数,这些对象成员的对象成员也不能有,以此类推。
  2. 联合体不能继承,因而也不支持包含多态。

一般联合体用来存储一些共有的数据,而不是为他定义函数成员。

联合体可以不声明名称,称为无名联合体。无名联合体没有标记名,只是声明一个成员项的集合,这些成员项具有相同的内存地址,可以由成员项的名字直接访问。

union {
    int i;
    float f;
}

在程序中可以这样使用:

i=10;
f=2.2;

无名联合体通常用作类或者结构体内嵌成员。

3.枚举类型——enum

C++包含两种枚举:不限定作用域的枚举类型和限定作用域的枚举类型类。

不限定作用域类型声明形式如下:

enum (枚举类型名) {变量值列表};

enum Weekday{SUN, MON, TUE, WED, THU, FRI, SAT};

4.位域

数据类型说明符 成员名 : 位数

不同编译器下,包含位于的类所占用的空间也会有所不同。

只有bool、char、int、enum的成员才能够被定义为位域。

位域虽然节省了内存空间,但是会耗费额外的操作。

……
private:
    unsigned number: 27;
    Level level: 2;
    Grade grade: 2;
……

5.用构造函数定义的类型转换

cout<<Line(Point(1), Point(4)).getLen()<<endl;
cout<<Line((Point)1, (Point)4).getLen()<<endl;
cout<<Line(static_cast<Point>(1), static_cast<Point>(4)).getLen()<<endl;
// 隐含转换
cout<<Line(1, 4).getLen()<<endl;

也可以使用以下方式避免隐含转换的发生:

explicit Point(int xx=0, int yy=0){
    x=xx;
    y=yy;
}

如果函数的实现类与函数在类定义中的声明是分离的,那么explicit关键字应当写在类定义中欧冠的函数原型声明处,而不能写在类定义外的函数实现处。 

6.对象作为函数参数和返回值的传递方式

fun(Point(1,2));
// 返回一个对象
Point fun1(){
    return Point(1, 2);
}

第三章 数据的共享和保护

一、作用域、可见性

1.函数原型作用域

在函数原型声明时形参的作用范围就是函数原型作用域。

2.局部作用域

函数形参列表中形参的作用域,从形参列表中的声明处开始,到整个函数体结束之处为止。

函数体内声明的变量,其作用域从声明处开始,一直到声明所在的块结束的花括号为止。

具有局部作用域的变量也称为局部常量。

3.类作用域

x.m或X::m

ptr->m

4.文件作用域

不在前述各个作用域中出现的声明,就具有文件作用域,这样声明的标识符其作用域开始于声明点,结束于文件尾。

5.命名空间作用域

为避免重名冲突,使编译器能够区分来自不同库的同名实体。

namespace namespace_name{
    // 代码声明
}

6.限定作用域的enum枚举类

定义限定作用域的枚举类型的方式是enum class {...},即多了class或struct限定符,此时枚举元素的名字遵循常规的作用域准则,即类作用域,在枚举类型的作用域外是不可访问的。相反,在不限定作用域的枚举类型中,枚举元素的作用域域枚举类型本身的作用于相同。

enum color {red, yellow, green};                // 不限定作用域的枚举类型
enum color1 {red, yellow, green};               // 错误,枚举元素重复定义
enum class color2 {red, yellow, green};         // 正确,限定作用域的枚举元素被隐藏了
color c = red;                                  // 正确,color的枚举元素在有效作用域中
color2 c1 = red;                                // 错误,color2的枚举元素不在有效的作用域中
color c2 = color::red;                          // 正确,允许显式地访问枚举元素
color2 c3 = color2::red;                        // 正确,使用了color2的枚举元素

具有文件作用域的变量也称为全局变量

7.可见性 

程序运行到某一点,能够引用到标识符,就是该处可见的标识符。

二、对象的声明周期

1.静态生命周期

如果对象的生命期与程序的运行期相同,我们称为它具有静态生存期。

static int i;

2.动态生存期

局部生命周期对象诞生于声明点,结束于声明所在的块执行完毕之时。

三、类的静态成员

1.静态数据成员

如果某个属性为整个类所共有,不属于任何一个具体对象,则采用static关键字来声明为静态成员。静态成员在每个类中只有一份,由该类的所有对象共同维护和使用,从而实现了同一类的不同对象之间的数据共享。

静态数据成员具有静态生命周期。

类名::标识符

2.静态函数成员

public:
    static void f(A a);

四、类的友元

 友元关系提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。

1.友元函数

class Point {                                     // Point类的定义
public:
    Point(int x=0, int y=0): x(x), y(){}            
    int getX(){return x;}
    int getY(){return y;}
    friend float dist(Point &p1, Point &p2);      // 友元函数声明
private:
    int x, y;
};


float dist(Poin &p1, Point &p2) {                // 友元函数实现
    double x = p1.x - p2.x;
    double y = p1.y - p2.y;
    return static_cast<float>(sqrt(x*x+y*y));
}

int main() {
    Point mypl(1, 1), myp2(4, 5);                    // 定义Point类的对象
    cout<<"The distance:";
    cout<<dist(myp1, myp2)<<endl;                    // 计算两点间的距离
    return 0;
}

运行结果
The distance is: 5

2.友元类

若A类为B类的友元类,则A类的所有成员函数都是B类的友元函数,都可以访问B类的私有和保护成员。

class B
{
    ...                        // B类的成员声明
    friend class A;            // 声明A为B的友元类
    ...
};
class A{
public:
    void display() {cout<<x<<endl;}
    int getX(){return x;}
    firend class B;                        // B类是A类的友元类
private:
    int x;
};

class B{
public:
    void set(int i);
    void display();
private:
    A a;
};

void B::set(int i){
    a.x = i;                                // 由于B是A的友元,所以在B的成员中可以访问A类对象的私有成员
}

需要注意的是:

  1. 友元关系是不能传递的
  2. 友元关系是单向的

五、共享数据的保护

1.常对象

常对象必须初始化,而且不能被更新。

const 类型说明符 对象名;
const A a(3, 4);

const int n = 10;          // 正确,用10对常量n进行初始化
n = 20;                    // 错误

2.用const修饰的类成员

(1)常成员函数

类型说明符 函数名(参数表) const;
  1. 如果将一个对象说明为常对象,则通过该常对象只能调用它的常成员函数,而不能调用其他成员函数。
  2. 无论是否通过常对象调用常成员函数,在常成员函数调用期间,目的对象都被视同为常对象,因此常成员函数不能更新目的对象的数据成员,也不能针对目的对象调用该类中没有用const修饰的成员函数。保证了常成员函数中不会更改目的对象的数据成员的值。
  3. const关键字可以用于重载函数的区分例如
    void print();
    void print() const;

    如果仅以const关键字为区分对成员函数重载,那么通过非const的对象调用该函数,两个重载的函数都可以与之匹配,这时编译器将选择最近的函数——不带const关键字的函数。

对于无需改变对象状态的成员函数,都应当使用const。

(3)常数据成员

const int a;
static const int b;        // 静态常数据成员


A::A(int i) : a{i}{}       // 常数据类型只能通过初始化列表来获得初值

类成员中的静态变量和常量都应当在类体之外加以定义,但C++标准规定了一个例外:类的静态常量如果具有整数类型或枚举类型,那么可以直接在类定义中为它指定常量值。

static const int b = 10;

3.常引用

常引用所引用的对象不能被更新。

const 类型说明符 &引用名;

一个常引用,无论绑定到一个普通的兑现,还是常对象,通过该引用访问该对象时都只能把该对象当作常对象,这意味着,对于基本数据类型的引用,则不能为数据赋值,对于类类型的引用,则不能修改它的数据成员,也不能调用它的非const的成员函数。

class Point{
public:
    Point(int x = 0, int y = 0): x(x), y(y){}
    int getX(){ return x; }
    int getY(){ return y; }
    friend float dist(const Point &p1, const Point &p2);
private:
    int x, y;
};

float dist(const Point &p1, const Point &p2){
    double x = p1.x - p2.x;
    double y = p1.y - p2.y;
    return static_cast<float>(sqrt(x * x + y * y));
}

int main(){
    const Point myp1(1, 1), myp2(4, 5);
    cout<<"The distance is: ";
    cout<<dist(myp1, myp2)<<endl;
    return 0;
}


运行结果:
The distance is:5

六、多文件结构和编译预处理命令

1.组织机构

三个文件:类定义文件(*.h)、类实现文件(*.cpp)、类的使用文件(*.cpp, 主函数文件)

2.外部变量和外部函数

(1)外部变量

// 源文件1
int i=3;            // 定义变量i
void next();        // 函数原型声明

int main(){
    i ++;
    next();
    return 0;
}


void next(){
    i ++;
    other();
}


// 源文件2
extern int i;      // 声明一个在其他文件中定义的外部变量i
void other(){
    i ++;
}

(2)外部函数

只要在调用之前进行引用性声明(即声明函数原型)即可。也可以在声明函数原型或定义函数时用extern修饰。

通常情况下,变量和函数的定义都放在源文件中,而对外部变量和外部函数的引用声明则放在头文件中。

(3)将变量和函数限制在编译单元内

使用匿名命名空间

namespace {
    int n;
    void f(){
        n++;    
    }
}

3.标准C++库

标准C++类与组件在逻辑上分为如下6种类型。

  • 输入输出类;
  • 容器类与ADT(抽象数据类型)
  • 存储管理类
  • 算法
  • 错误处理
  • 运行环境支持

在使用标准C++库时,还需要加入下面这一条语句来将指定命名空间中的名称引入当前作用域中:

using namespace std;

也可以使用std::

通常,using namespace语句不应该放在头文件中,因为这回使得一个命名空间不被察觉地对一个源文件开放。

4.编译预处理

(1)#include指令

  • #include<文件名>:按标准方式搜索,文件位于系统目录的include子目录下。
  • #include"文件名":首先在当前目录中搜索,若没有,再按标准方式搜索。

(2)#define和#undef指令

在C++中可以使用const函数代替#dedfine PI 3.14,使用内联函数取代宏定义

#undef作用是删除#define定义的宏。

(3)条件编译指令

// 形式1
#if 常量表达式
    程序段    // 当“常量表达式”非零时编译本段
#endif


// 形式2
#if 常量表达式
    程序段1    // 当“常量表达式”非零时编译本程序段
#else
    程序段2    // 当“常量表达式”为零时编译本程序段
#endif


// 形式3
#if 常量表达式1
    程序段1        // 当“常量表达式1”非零时编译本程序段
#elif 常量表达式2
    程序段1        // 当“常量表达式1”为零、“常量表达式1”非零时编译本程序段
……
#else
    程序段n+1      // 其他情况编译本程序段
#endif


// 形式4
#ifdef 标识符    // 如果标识符经#defined定义过,且未经undef删除,则编译程序端1
    程序段1
#else   
    程序段2
#endif

// 形式5
#ifndef 标识符    // 如果标识符未被定义过,则编译程序段1,否则编译程序段2
    程序段1
#else
    程序段2
#endif

(4)defined操作符

defined是一个预处理操作符,而不是指令,因此不要以#开头。defined操作符使用的形式为:

defined(标识符)

若“标识符”在此前经#define定义过,并且未经过#undef删除,则上述表达式为非0,否则上述表达式的值为0.

#ifndef HEAD_H
#define HEAD_H

class Point{
    ……
}

5.常成员函数的声明原则

使用mutable关键字后,就可以将getLen函数声明为常成员函数。

class Line{
public:
    Line(const Point &p1, const Point &p2) : p1(p1), p2(p2), len(-1) {}
    double getLen() const;
private:
    Point p1, p2;
    mutable double len;
};

double Line::getLen() const {
    if(len<0){
        double x = p1.getX() - p2.getX();
        double y = p1.getY() - p2.getY();
        len = sqrt(x * x + y * y);
    }
    return len;
}

6.代码的编译、连接与执行过程

(1)编译 

(2)连接

(3)执行

第四章 数组、指针与字符串

一、数组

1.数组的声明

数据类型 标识符[常量表达式1][常量表达式2]...;
int b[10];
int a[5][3];

int c[] = {1, 1, 1};
int c[3] = {1, 1, 1};

2.数组作为函数参数

使用数组名传递数据时,传递的是地址。

void rowSum(int [][4], int nRow) {
    ……
}

int main(){
    int table[3][4] = {{1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6}};
    ……
    rowSum(table, 3);
}

3.对象数组

类名 数组名[常量表达式];

数组名[下标表达式].成员名

Location a[2] = {Location(1, 2), Location(3, 4)};

a[i].move(i+10, i+20);

二、指针

1.指针变量的声明

数据类型 *标识符;
int a;
int *ptr;
ptr = &a;
int b;
int *ptr2 = &b;

2.指针赋值

// 在定义指针的同时进行初始化赋值
存储类型 数据类型 *指针名=初始地址;
// 在定义之后,单独使用赋值语句
指针名 = 地址;

int a[10];
int *ptr = a;

数组名称实际上就是一个不能被赋值的指针,即指针常量。

(1)可以生命指向常量的指针,此时不能通过指针来改变所指对象的值,但指针本身可以改变,可以指向另外的对象。

int a;
const int *p1 = &a;    // p1是指向常量的指针
int b;
p1=&b;                 // 正确,p1本身的值可以改变
*p1=1;                 // 编译出错,不能通过p1改变所指的对象

 (2)可以声明指针类型的常量,这时指针本身的值不能被改变

int * const p2 = &a;
p2 = &b;                // 错误,p2是指针常量,只能不能改变

(3)void类型指针,可以存储任何类型的对象地址。

3.指针运算 

0专用于表示空指针,也就是一个不指向任何有效地址的指针。

int *p;
p=0;

int *p = NULL;

4.用指针处理数组元素

int array[5];
// 作为形参的时候可以是
void f(int p[]);
void f(int p[3]);
void f(int *p);

5.标准库函数begin和end

int a[10] = {1, 2, 3, 4, 5 ,6 ,7, 8, 9, 0};
int *beg = begin(a);
int *last = end(a);

begin函数返回指向数组a首元素的指针,end函数返回指向a尾元素下一位置的指针,这两个文件定义在iterator头文件中。

6.指针数组

数据类型 * 数组名[下表表达式];
int *pa[3];

7.用指针作为函数参数

void splitFloat(float x, int * intPart, float *fracPart){
    * intPart = static_cast<int>(x);
    * fracPart  x - * intPart;
}


int main(){
    cout<<"Enter 3 float point numbers: "<<endl;
    for(int i = 0; i < 3; i++){
        float x, f;
        int n;
        cin>>x;
        splitFloat(x, &n, &f);
        cout<<"Integer Part ="<<n<<"Fraction Part="<<f<<endl;
    }
    return 0;
}

8.指针型函数

数据类型 *函数名(参数表){
    函数体
}

typedef int arr[10];
using arr=int[10];
arr* foo(int i);

如果不使用类型别名定义一个返回数组指针:

类型说明符 (* 函数名(参数表))[数组维度]

int (* foo(int i))[10]

可以按照以下顺序逐层理解这个函数的含义:

  1. foo(int i)定义了一个函数foo,需要一个int类型的参数
  2. (* foo(int))意味者对函数返回的结果执行解析操作
  3. int (*foo(int i))[10]说明数组是int类型

C++ 11标准提供了一种简化方法

auto foo(int i) -> int (*)[10];

如果知道函数返回的指针指向哪个数组,就可以使用decltype关键字声明返回类型。

int a[] = {0, 1, 2, 3, 4};
int b[] = {5, 6, 7, 8, 9};

decltype(a) * func(int i){
    return (i%2) ? &a: &b;
}

func前的符号“*”表示返回类型是一个,decltype表示指针所指对象与a的类型一致,由于a是数组,因此func返回了一个指针指向含有5个整数的数组。

9.指向函数的指针

数据类型 (* 函数指针名)(形参表)

// 可以使用typedef
typedef int (* DoubleIntFunction)(double);

// 需要声明这一类变量可以直接使用
DoubleIntFunction funcPtr;

函数在使用之前也要进行赋值,使指针指向一个已经存在大的函数代码起始地址。

函数指针名给=函数名;
void printStuff(float) {
    cout<<"This is the print stuff function."<<endl;
}

void printMessge(float data){
    cout<<"The data to be listed is "<<data<<endl;
}

void printFloat(float data){
    cout<<"The data to be printed is "<<data<<endl;
}


const float PI = 3.14;
const float TWO_PI = PI * 2.0f;

int main(){
    void (*functionPointer)(float);
    printStuff(PI);
    functionPointer=printStuff;
    functionPointer(PI);
    functionPointer=printMessage;
    functionPointer(PI);
    functionPointer(13.0);
    functionPointer=printFloat;
    functionPointer(PI);
    printStuff(PI);
    return 0;
}

10.对象指针

(1)对象指针的一般概念

和基本类型的变量一样,每个对象在初始化之后都会在内存中占有一定的空间。因此,既可以通过对象名,也可以通过对象地址来访问一个对象。

类名 * 对象指针名;

Point *pointPrt;
Point p1;
pointPrt=&p1;

// 访问对象成员
对象指针名->成员名

(2)this指针

(3)指向类的非静态成员指针

类型说明符 类名::*指针名;                // 声明指向数据成员的指针
类型说明符 (类名::*指针名)(参数表);       // 声明指向函数成员的指针

// 对其赋值
指针名=&类名::数据成员名;

对类成员取地址时,也要遵守访问权限的约定,也即是说,在一个类的作用域之外不能够对它的私有成员取地址。

对象名.* 类成员指针名
对象指针名->* 类成员指针名

// 使用以下形式赋值
指针名=&类名::函数成员名;

经过上述对成员函数指针赋值以后,还不能用指针直接调用成员函数。使用以下形式的语句利用指针调用成员函数。

(对象名.* 类成员指针名)(参数表)
(对象指针名->* 类成员指针名)(参数表)
int main(){                                        // 主函数
    Point a(4, 5);                                 // 定义对象A
    Point *p1=&a;                                  // 定义对象指针并初始化
    int (Point:: *funcPtr)() const=&Point::getX;   // 定义成员函数指针并将其初始化

    cout<<(a.* funcPtr)()<<endl;                   // (1)使用成员函数指针和对象名访问成员函数
    cout<<(p1->* funcPtr)()<<endl;                 // (2)使用成员函数指针和对象指针访问成员函数
    cout<<a.getX()<<endl;                          // (3)使用对象名访问成员函数
    cout<<p1->getX()<<endl;                        // (4)使用对象指针访问成员函数
    
    return 0;
}

(4)指向类的静态成员的指针

int main(){
    int *ptr=&Point::cout;
    ...
}

11.动态内存分配

new 数据类型(初试化参数列表);

int *point;
point = new int(2);

运算符delete用来删除由new建立的对象,释放指针所指向的内存空间。

delete 指针名;

delete point;

使用运算符new也可以创建数组类型的对象,需要给出数组的结构说明,用new运算符动态创建一维数组的语法为:

new 类型名[数组长度];

int * p = new int[10]();
delete []指针名;

12.用vector创建数组

vector<元素类型> 数组对象名(数组长度);
int x = 10;
vector<int> arr(x);
vector<元素类型> 数组对象名(数组长度, 元素初值);

对vector数组对象元素的访问方式,与普通数组具有相同形式:

数组对象[下标表达式]

13.深层复制与浅层复制

string();       // 默认构造函数,建立一个长度为0的串                                                
string(const strings &rhs);    // 复制构造函数
string(const char *s);         // 用指针s所指向的字符串常量初始化string类的对象
string(const string& rhs, unsigned int pos, unsigned int n);    // 将对rhs中的串中从位置pos开始取n个字符,用来初始化string类的对象
string(const char *s, unsigned int n);    // 用指针s所指向的字符串的前n个字符串初始化string类的对象
string(unsigned int n, char c);    // 将参数c中的字符重复n次,用来初始化string类的对象

二、字符串

1.用字符数组存储和处理字符串

const char * STRING1 = "This is a string.";
const str[8] = { 'p', 'r', 'o', 'g', 'r', 'a', 'm' };
char str[8] = "program";
char str[] = "program";

 2.string类

(1)构造函数和原型

string();       // 默认构造函数,建立一个长度为0的串                                                
string(const strings &rhs);    // 复制构造函数
string(const char *s);         // 用指针s所指向的字符串常量初始化string类的对象
string(const string& rhs, unsigned int pos, unsigned int n);    // 将对rhs中的串中从位置pos开始取n个字符,用来初始化string类的对象
string(const char *s, unsigned int n);    // 用指针s所指向的字符串的前n个字符串初始化string类的对象
string(unsigned int n, char c);    // 将参数c中的字符重复n次,用来初始化string类的对象

(2)string类的操作符

(3)常用成员函数功能简介

string append(const char *s);    // 将字符串s添加在本串尾
string assign(const char *s);    // 赋值,将s所指向的字符串赋值给本对象
int compare(const string &str) const    // 比较本串与str串的大小,当本串<str串时,返回负数,当本串>str串时,返回正数,两串相等时,返回0
string & insert(unsigned int p0, const char * s);    // 将s所指向的字符串插入在本串中位置p0之前
string substr(unsigned pos, unsigned int n) const;    // 取子串,取本串中位置pos开始的n个字符,构成新的string类对象作为返回值
unsigned int find(const basic_string & str) const    // 查找并返回str在本串第一次出现的位置
unsigned int length() const;    //返回串长度
void swap(string &str);    // 将本串与str中的字符串进行交换

四、指针与引用

1.指针与引用

指针常量与引用使用形式的比较
操作T类型的指针常量对T类型的引用

定义并用v初始化(v是T类型变量)

取v的值

访问成员m

读取v的地址

T *const p=&v;

*p

p->m

p

T &r=v;

r

r.m

&r

2.指针的安全性隐患及其应对方案

使用指针运算符时常常会出现越界的异常。

3.类型安全性

4.堆对象的管理

用new在程序运行时动态创建的堆对象必须由程序用delete显示删除。

5.const_cast的应用

void foo(const int *cp) {
    int *p = const_cast<int *> (cp);
    (* p)++;
}

该段代码使用const_cast,将cp类型中的const去除,将常指针cp转换为普通指针p,然后通过p修改它所指向的变量。

第五章 类的继承

一、基类与派生类

1.派生类

class 派生类名: 继承方式 基类名1, 继承方式 基类名2, ……, 继承方式 基类名n
{
    派生类成员声明;
};
图 多继承和的单继承的UML表示

直接参与派生出某类的基类称为直接基类,基类的基类甚至更高层的基类称为间接基类。。

继承方式规定了如何访问从基类继承的成员。

2.访问控制

(1)公有继承

当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问。

(2)私有继承

当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。

(3)保护继承

保护继承中,基类的公有和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可直接访问。

 3.类型兼容规则

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。

4.派生类的构造和析构函数

(1)构造函数

构造派生类的对象时,就要对基类的成员对象和新增成员对象进行初始化。先调用基类的构造函数,在执行派生类的构造函数。

class Base1{
public:
    Base1(int i){
        cout<<"Constructing Base1 "<<i<<endl;
    }
};
class Base2{
public:
    Base2(int j){
        cout<<"Constructing Base2 "<<j<<endl;
    }
};
class Base3{
public:
    Base1(){
        cout<<"Constructing Base3 "<<endl;
    }
};

class Derived: public Base2, public Base1, public Base3{
public:
    Derived(int a, int b, int c, int d): Base1(a),member2(d), member1(c) Base2(bb){}
private:
    Base1 member1;
    Base2 member2;
    Base3 member3;
    
};

int main(){
    Derived obj(1, 2, 3, 4);
    return 0;
}

在C++ 11标准中,派生类能够重用其直接基类定义的构造函数。

class Derived: public Base{
public:
    using Base::Base;
    double d;
}

 通常情况下,using声明语句将指令编译器产生如下代码:

Derived(params): Base(arg){}

复制构造函数 

假设Derived类是Base类的派生类,Derived类的复制构造函数形式如下:

Derived::Derived(const Derived &v): Base(v) {...}

(2)析构函数

析构函数执行次序和构造函数正好相反,首先执行析构函数的函数体,然后对派生类新增的类型类型的成员对象进行清理,最后对所有从基类继承而来的成员进行清理。 

(3)删除delete构造函数

class Base{
public:
    Base()=default;
    Base(string_info): info(std::move(_info)){}
    Base(Base &)=delete;
    Base(Base &&)=delete;
private:
    string info;
};

class Derived::public Base{};

Derived d1;                // 正确,合成了默认构造函数
Derived d2(d1);            // 错误,删除了复制构造函数
Derived d3(std::move(d1))  // 错误,删除了移动构造函数

5.派生类成员的标识与访问

(1)作用域分辨符

类名::成员名            // 数据成员
类名::成员名(参数名)     // 函数成员

如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果某派生类的多个基类拥有同名的成员,同时,派生类有新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员。 

6.虚基类 

可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个,同一个函数也只有一个映射。

class 派生类名: virtual 继承方式 基类名

在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。声明了虚基类之后,虚基类的成员再进一步派生过程中和派生类一起维护同一个内存数据。

7.虚基类及其派生类构造函数

如果虚基类声明了带参数的构造函数,程序就要改成如下形式:

class Base0{
public:
    Base0(int var): var0(var){}
    int var0;
    void fun0(){
        cout<<"Member of Base0"<<endl;
    }
};
class Base1: virtual public Base0{
public:
    Base1(int var):Base0(var){}
    int var1;
};
class Base2: virtual public Base0{
public:
    Base2(int var): Base0(var){}
    int var2;
};
class Derived: public Base1, public Base2{
public:
    Derived(int vaar): Base0(var), Base1(var), Base2(var){}
    int var;
    void fun(){cout<<"Member of Derived"<<endl;}
};

int main(){
    Derived d(1);
    d.var = 2;
    d.fun();
    return 0;
}

 第六章 多态性

一、运算符重载

 运算符重载实质就是函数重载。

1.运算符重载的规则

返回类型 类名::operator 运算符(形参表)
{
    函数体
}+

运算符重载为非成员函数的一般与非形式为:

返回类型 operator 运算符(形参表)
{
    函数体
}

2.运算符重载为成员函数

重载为成员函数,它就可以自由地访问本类的数据成员。

……
Complex operator+(const Complex &c2) const;
Complex operator-(const Complex &c2) const;
……
Complex Complex::operator+(const Complex &c2) const
{
    return Complex(real+c2.real, imag+c2.imag);
}
Complex Complex::operator-(const Complex &c2) const
{
    return Complex(real-c2.real, imag-c2.imag);
}

对于单目运算符++而言,可以通过如下方式: 

……
Clock& operator++();        // 前置单目运算符重载
Clock operator++(int);      // 后置单目运算符重载
……

Clock & Clock::operator++(){
    ……
}

Clock Clock::operator++(int){
    Clock old = *this;    
    ++(*this);
    return old;
}

3.运算符重载为非成员函数

class Complex{
public:
    ……
    friend Complex operator+(const Complex &c1, const Complex &c2);
    friend Complex operator-(const Complex &c1, const Complex &c2);
    friend ostream &operator<<(ostream  &out, const Complex &c);
……

};

Complex operator+(const Complex &c1, const Complex &c2){
    return Complex(c1.real+c2.real, c1.imag+c2.imag);
}

Complex operator-(const Complex &c1, const Complex &c2){
    return Complex(c1.real-c2.real, c1.imag-c2.imag);
}


ostream &operator<<(ostream &out, const Complex &c){
    out<<"("<<c.real<<", "<<c.imag<<")";
    return out;
}

……

二、虚函数

1.一般虚函数成员

virtual 函数成员 函数名(形参表);

虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。运行过程中多态需要满足三种条件:赋值兼容规则、声明虚函数、有成员函数来调用或者是通过指针、引用来访问函数。  

final和override

class Base{
public:
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
class Derived1: public Base{
public:
    void f1(int) const override;      // 正确,f1
与基类中的f1匹配
    void f2(int) override;            // 错误,基类中没有形如f2(int)的函数
    void f3() overeide;               // 错误,f3不是虚函数 
    void f4() override;               // 错误,基类中没有名为f4的函数 
}
class Derived2: public Base{
public:
    void f1(int) const final;    // 不允许后续的其他类覆盖f1(int)
};
class Derived3: public Derived2{
public:
    void f1(int) const;        // 错误,Derived2的f1已经声明为final
    void f2();                 // 正确,覆盖从Base间接继承来的f2   
}

2.虚析构函数

virtual ~类名();

 三、纯虚函数与抽象类

1.纯虚函数

声明为纯函数之后,基类中就可以不再给出函数的实现部分。

virtual 函数类型 函数名(参数表)=0;

2.抽象类

带有纯虚函数的类是抽象类。抽象类不能实例化。

class Base1{
public:
    virtual void display() const = 0;
}

第7章 模板与群体数据

按照对象的方法将数据与操作封装起来,这就是群体类。数组就是一种群体数据的存储结构。群体数据分为两种:线性群体数据和非线性群体数据。线性群体数据中的元素按位置排列有序。非线性群体不用位置顺序来标识元素。

一、函数模板与类模板

1.函数模板

template <模板参数表>
类型名 函数名(参数表)
{
    函数体定义
}

template <typename T>
T abs(T x){
    return x<0? -x: x;
}

int main(){
    int n = -5;
    double d = -5.5;
    cout<<abs(n)<<endl;
    cout<<abs(d)<<endl;
    return 0;
}


template <class T>
void outputArray(const T *array, int count){
    ……
}

2.类模板

template <模板参数表>
class 类名
{
    类成员声明
}

如果需要在类模板以外定义其成员函数,则要采用以下的形式:

template <模板参数表>
类型名 类名<模板参数标识符列表>::函数名(参数表)

使用一个模板类来建立对象时,应按照如下形式声明:

模板名 <模板参数表> 对象名1, 对象名2, ……,对象名n;

二、线性群体

1.直接访问群体——数组类 

2.顺序访问群体——链表类

3.栈类

4.队列类

三、群体数据的组织

1.插入排序

2.选择排序

3.交换排序

4.顺序查找

5.折半查找

第八章 泛型程序设计与C++语言标准模板库

一、迭代器

1.输出流迭代器和输出流迭代器

(1)输入流迭代器

它是一个类模板:

template <class T> istream_iterator<T>;

一个输入流迭代器的实例需要有下面的构造函数来构造:

istream_iterator(istream &in);

(2)输入流迭代器

template <class T> ostream_iterator<T>;

它有两个构造方法:

ostream_iterator(ostream &out);
ostream_iterator(ostream &out, const char * delimiter);
double square(double x){
    return x * x;
}

int main(){
    transform(istream_iterator<double>(cin), istream_iterator<double>(), ostream_iterator<double>(coutm "\t"), square);
    cout<<endl;
    return 0;
}

2.迭代器的分类

(1)输入迭代器

(2)输出迭代器

(3)前向迭代器

(4)双向迭代器

(5)随机访问迭代器

3.迭代器的区间

4.迭代器的辅助函数

STL为迭代器提供了两个辅助函数模板——advacne和distance,它们为所有迭代器提供一些原本只有随机访问迭代器才有的访问能力,前进或后退多个元素,以及计算两个迭代器之间的距离。

三、容器的基本功能分类

容器的基本功能如下:

  • S s1        容器都有一个默认构造函数,用于构造一个没有任何元素的空容器
  • slop s2        这里op可以是==、!=、<、<=、>、>=、之一,它会对两个容器之间的元素按字典顺序进行比较。
  • s1.begin()        返回指向s1第一个元素的迭代器
  • s1.end()        返回指向s1最后一个元素的下一个位置的迭代器
  • s1.clear()        将容器s1的内容清空
  • s1.empty()        返回一个布尔值,表示s1容器是否为空
  • s1.size()        返回s1的元素个数
  • s1.swap(s2)        将s1容器和s2容器的内容交换
STL中各种容器头文件和所属概念
容器名中文名头文件所属概念
vector向量<vector>随机访问容器,顺序容器
deque双端队列<deque>随机访问容器,顺序容器
list列表<list>可逆容器,顺序容器
forward_list单项链表<forward_list>单向访问容器,顺序容器
array数组<array>随机访问容器,顺序容器
set集合<set>可逆容器,关联容器
multiset多重集合<set>可逆容器,关联容器
map映射<map>可逆容器,关联容器
multimap多重映射<map>可逆容器,关联容器
unordered_set无序集合<unordered_set>可逆容器,关联容器
unordered_multiset无序多重集合<unordered_set>可逆容器,关联容器
unordered_map无序映射<unordered_map>可逆容器,关联容器
unordered_multimap无序多重映射<unordered_map>可逆容器,关联容器

1.顺序容器

vector、deque、list、forward_list、array。

2.关联容器

set、map、multiset、multimap、unordered_set、unordered_multiset、unordered_map、unordered_multimap

四、函数对象

本质上是一个类,不是一个函数。函数对象重载了小括号。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值