C/C++后台开发面试题总结

一、C/C++语言:

1. class访问属性:

private: 私有成员,只有本类内的成员函数可以访问;
public: 公有成员,类作用域中的所有函数都可以访问;
protected: 受保护的成员,类外不可以访问,派生类可以访问。

private、public、protected这几个访问限制说明符的作用就是要实现C++类的“封装性”—— 将私有的数据成员声明为private,这部分数据对外部对象不可见,而public则是类为外部使用者提供的访问类内数据的方式的接口,想要访问类的私有成员,必须通过类指定的public接口函数。

class Sales_data {
public:
	std::string ShowIsbn() { std::cout << isbn; }
	double ShowPrice() { std::cout << price; }
private:
	std::string 	isbn;
	double 			price;
};

例如对于上面的一个用于描述商品信息的类,每个商品都是一个实例化的Sales_data类型的对象,类外使用者只能通过ShowIsbn()函数和ShowPrice()函数查看商品价格,想要擅自修改商品价格是不行的。


2. 继承方式:

public公有继承: 基类的公有成员和受保护成员在派生类中保持原有的访问属性,基类的私有成员仍然为基类的私有;

private私有继承: 基类的公有成员和保护成员在派生类中成了私有成员,基类的私有成员仍然为基类私有;

protected保护继承: 基类的公有成员和保护成员在派生类中成了保护成员,私有成员仍然为基类私有。

总之,无论是三种继承方式中的哪一种,被继承的基类中的私有成员永远都为基类私有,访问这部分成员必须通过基类的public接口函数,即使是在派生类内想要访问基类的私有成员,也必须通过基类指定的方式,即基类的public接口函数。这符合类的封装性。


3. inline与类的成员函数的关系:

函数的调用涉及到堆栈的操作,对于一些频繁被调用的小规模函数,函数调用切换的开销要远远大于顺序执行的开销,对于这类函数可以将其声明为inline内联函数,在编译时将其进行展开。

在类内定义的成员函数,且其中不包含循环、递归、条件等复杂操作时,编译器会默认将其当作内置函数(inline)来处理。

例如:

class Sales_data {
public:
	//类内定义,是inline内联函数:
    std::string showIsbn() { std::cout << isbn; }

	//类内声明,类外定义,不是inline函数:
	double showPrice();
};

double Sale_data::showPrice() {
    std::cout << price;
}

4. 编译器对inline函数的处理步骤及inline函数的优缺点:

编译器对inline函数的处理步骤:
(1)将inline函数体复制到inline函数调用点处;
(2)为所用inline函数中的局部变量分配内存空间(栈空间);
(3)将inline函数的输入参数和返回值映射到调用方的局部变量空间中;
(4)如果inline函数有多个返回点,将其转变为inline函数代码块末尾的分支(使用goto)。

优缺点:
优点:
(1)内联函数同宏函数(#define)一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收、结果返回 等步骤,从而提高了程序的运行速度
(2)内联函数相比于宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏函数则不会;
(3)在类中定义声明的成员函数,自动转换为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能;
(4)内联函数在运行时可调试,而宏定义则不能。

缺点:
(1)代码膨胀。 每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间
(2)inline函数无法随着函数库的升级而升级。inline函数的改变需要重新编译,不能向non-inline可以直接链接
(3)是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在编译器手里。


5. 字符串数组与字符串指针,sizeof()与strlen():

C语言中是没有类似与C++、Java等语言中的string那样的特定的字符串类型的,C语言是将字符串放进一个字符数组中。

对于字符数组的初始化,有两种形式:

//(1)用 "字符常量" 逐个初始化数组中的元素:
char array[] = {'h', 'e', 'l', 'l', 'o'};
//(2)用 "字符串常量" 初始化整个数组:
char array[] = "hello"; //初始化后的结果:array[] = {'h', 'e', 'l', 'l', 'o', '\0'};

注意第二种使用“字符串常量”初始化字符串数组的方式,会在数组尾部加上一个 ‘\0’ 字符。

字符串常量在程序的内存布局中是存放在常量区的,字符数组在被初始化时,相当于是字符串常量的一个副本(存在在栈区或堆区)。

另外,字符串数组与字符串指针 之间也存在很大的差别:

char amessage[] = "now is the time";	//定义一个数组
char *pmessage = "now is the time";		//定义一个指针

上述声明中,amessage是一个仅仅足以存放初始化字符串以及空字符’\0’的一维数组。数组中的单个字符可以进行修改,但amessage始终指向同一个存储位置。

另一方面,pmessage是一个指针,其初值指向一个字符串常量,指针的指向可以修改,但如果试图通过指针来修改字符串的内容,结果是未定义的。

strlen和sizeof可以用来计算字符串的长度:

strlen: 从内存的某个位置开始扫描,直到碰到第一个字符串结束符’\0’为止,然后返回计数器值(长度不包含’\0’);

sizeof:数组,计算得到整个数组所占空间的大小;
对指针,得到指针本身所占空间的大小。

示例:

#include <iostream>
  
int main() {

    char array[] = {'h','e','l','l','o'};
    char arr[] = "hello";
    char *str = "hello";

    std::cout << sizeof(array);		//5 计算的是数组的大小
    std::cout << sizeof(arr);		//6
    std::cout << sizeof(str);		//8

    std::cout << strlen(array);		//?? 末尾没有'\0',strlen得到的值随机
    std::cout << strlen(arr);		//5
    std::cout << strlen(str);		//5

    return 0;
}

------

输出结果:

5
6
8
5
5
5

验证 字符串常量 与 字符串数组 在内存布局中的存储位置:

#include <stdio.h>

int main() {
    char a = 'c';
    char *atr = &a;
    char *ptr = "hello";

    printf("%p, %p\n\n\n", atr, ptr);
}

------

0x7ffee24f9a6b, 0x10d709f74

“字符串指针”就把它理解成一个指向常量区的指针,它的指向可以变,但是当它指向常量区时不能所指对象的值。

*atr指向的栈空间,*ptr指向的是常量区,二者的地址差距巨大。


6. union联合体:

联合(union)是一种节省空间的特殊的类,一个union可以有多个数据成员,共用体中的所有成员共享同一个空间,并且同一时间只能储存其中一个成员变量的值。

同struct一样,union 的默认访问权限也是public公有的,并且也可以在union 内声明/定义成员函数。


7. Linux下C++文件编译与执行的四个步骤:

预编译: 头文件替换、宏定义展开等预处理命令替换;(.c/.cpp -> .i)
编译: 源文件语言编译成汇编语言;(.i -> .s)
汇编: 把汇编语言翻译成目标机器指令;(.s -> .o)
链接: 链接目标文件生成可执行文件。(.o -> .out)


8. 引用和指针的区别:

(1)引用是变量一个别名,内部实现是 只读指针
(2)引用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变;
(3)引用不能为NULL,指针可以为NULL;
(4)引用变量内存单元保存的是被引用变量的地址;
(5)“sizeof引用”=指向变量的大小,“sizeof指针”=指针本身的大小;
(6)引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址;
(7)引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量的地址。


9. vector内存的分配方式:

先申请一定大小的数组,当数组填满之后,另外申请一块原数组两倍大小的新数组,然后把原数组的数据拷贝到新数组,最后释放原数组的内存。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;
    
    for(int i = 0; i < 10; i++) {
        cout << i << ' ' << v.size() << ' ' << v.capacity() << endl;
        v.push_back(i);
    }

    return 0;
}

------

0 0 0
1 1 1
2 2 2
3 3 4
4 4 4
5 5 8
6 6 8
7 7 8
8 8 8
9 9 16

10. vector和list的特点、区别:

  1. 底层存储实现上的区别:
      总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
    如果需要大量的插入和删除,而不关心随机存取,则应使用list。
      vector和数组类似,拥有一块连续的内存空间,并且起始地址不变,因此能高效的进行随机存取,时间复杂度是O(1);
      但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n)。
    另外,当数组空间不足时,会重新申请一块内存空间并进行内存拷贝。
      list是由双向链表实现的,因此内存空间是连续的。只能通过指针访问数据,所以list的随机存取效率很低,时间复杂度是O(n);
      但由于链表的特点,能高效的进行插入和删除,时间复杂度是O(1)。
  2. vector和list迭代器的区别:
      vector拥有一块连续的内存空间,能支持随机存取,所以 vector< int>::iterator 支持 “+”, “+=”, “<” 等操作符;
      list的内存空间是不连续的,因此它不支持随机存取,所以 list< int>::iterator 不支持 “+”, “+=”, “<” 等操作符;
    vector< int>::iterator 和 list< int>::iterator 都重载了 "++"运算符。
vector<int> v;
list<int> l;

cout << (v.begin() < v.end()) << endl;
cout << *(v.begin() + 1) << endl;
cout << v[2] << endl;

//++操作
for(vector<int>::iterator itv = v.begin(); itv != v.end(); itv++) {

}
for(vector<int>::iterator itl = l.begin(); itl != l.end(); itl++) {

}


11. 哪些函数不能被声明为虚函数:

在C++中,有五种函数不能被声明为虚函数,分别是:
非成员函数、构造函数、静态成员函数、内联成员函数、友元函数。

总之一个原则,C++引入虚函数的目的是为了实现运行时多态,使用基类的指针分别调用基类和派生类中的virtual虚函数时可以产生不同的结果。因此,没有继承特性的函数,就没有虚函数的说法。

(1)非成员函数:
虚函数针对的是类内的成员函数,非成员函数只能被重载(overload),不能被继承(override)。非成员函数在编译期间就已经绑定函数了。

(2)构造函数:
要想调用虚函数,必须要通过类的“虚函数表”进行,而虚函数表是要在对象实例化之后才能够调用(虚函数表指针(vptr)是含有虚函数的类的隐式成员,类对象在初始化时,构造函数负责初始化虚函数表指针指向类的虚函数表(virtual function table)地址)。
在构造函数运行期间,还没有为虚函数表分配空间,自然就没法调用虚函数了。

(3)静态成员函数:
静态成员函数对于每个类来说只有一份,所有的对象都共用这一份代码,它是属于类的而不是属于对象。虚函数必须根据对象类型才能知道调用哪一个虚函数,故虚函数是一定要在对象基础上才可以的,两者一个是与实例相关,一个是与类相关。
(个人理解,static静态成员函数是在编译期确定,函数地址保存在代码区中,而虚函数是需要放在虚函数表中,在编译时被动态绑定的,C++在实现时并没有把静态函数放在虚函数表中,而是放在了静态区)

(4)内联成员函数:
内联函数会在编译时被展开,虚函数在运行时才能动态的绑定函数。

实际上,virtual和inline可以共存,编译时可以通过,inline只是对编译器的一个建议,至于是否真的将这个函数作为inline,需要根据汇编语言查看。

#include <iostream>
using namespace std;

class Base {
public:
    virtual inline void print() { cout << "base func" << endl; }
};

class Derived : public Base {
public:
    virtual inline void print() { cout << "derived func" << endl; }
};

int main() {
    Base b;
    Derived d;

    Base *ptr = &b;
    ptr->print();
    ptr = &d;
    ptr->print();

    return 0;
}

------

base func
derived func

(5)友元函数:
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。并且友元函数也不属于类的成员函数,不能被继承。


12. class与struct的区别:

C++中的class类默认的成员是私有的,struct默认的是公有的。

注意:
网上有人说 “C++中的class类可以定义成员函数, struct只能定义成员变量”,这种说法是错误的:在C++中,struct结构体中同样可以定义成员函数,同样可以通过 访问说明符(access specifiers)public、private 来实现封装。

#include <iostream>
using namespace std;

//用struct关键字来实现一个类:
struct Demo {
public:
    void print() {
        cout << "hello" << endl;
    }
private:
    int a;
};

int main() {
    struct Demo dm;
    dm.print();

    return 0;
}

------

hello

在《C++ Primer》一书中7.2节P240中有关于class与sruct的描述:

使用class或strut关键字:
  在上面的定义中我们还做了一个微秒的变化,我们使用了class关键字而非struct开始类的定义。这种变化仅仅是形式上有所不同,实际上我们可以使用这两个关键字中的任何一个定义类。唯一的一点区别是,struct和class的默认访问权限不太一样。
  类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果我们使用class关键字,则这些成员是private的。

使用class和struct定义类唯一的区别就是默认的访问权限。


13. 变量的声明和定义有什么区别:

声明变量不分配空间,定义变量要分配空间。
声明主要是告诉编译器,后面的引用都按声明的格式。定义其实包含了声明的意思,同时要分配内存空间。


14. 堆和栈的解释和区别:

在理解堆和栈这两个概念时,需要放到具体的场景下。一般情况下,有两层含义:
(1)程序内存布局 场景下,堆与栈表示的是 两种 内存管理方式
(2)数据结构 场景下,堆与栈表示两种常用的数据结构。

栈: 由操作系统自动分配、释放用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈;

堆: 由程序员分配、释放,若程序员没有显式释放,则在程序结束时由操作系统回收,分配方式类似于链表。


15. 指针与数组名的区别:

二者的中都是一个地址,数组名表示的是数组首地址的指针,但这个指针很特别:
(1)数组名只能指向数组的首地址,它不能被移动,例如有 char a[]; ,如果出现 a = a+1 这种移动指针的操作将会编译不通过;
(2)当使用数组名这个指针的时候,系统会传入数组的信息,例如对数组名求sizeof得到的是整个数组的长度,而对指针求sizeof的结果是指针本身的长度:

char a[5];
char *p = a;
cout << sizeof(a);		//5
cout << sizeof(p);		//4

16. 左值、右值、左值引用、右值引用:


17. OOP的三个核心思想:

面向对象程序设计(OOP)的核心思想是:数据抽象、继承、动态绑定。(或者说:封装、继承、多态)

封装,可以将类的接口与实现分离
继承,可以定义相似但不完全相同的类型,一定程度上实现了代码复用;
多态,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象,同样是实现了代码复用,同样的一套代码实现不同的效果。


18. overload 重载、overwrite 重写、override 覆盖,三者区别:

三者描述的都是“函数名相同”的多个函数之间的关系。

18.1 overload 重载:

如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为 “重载函数”(overload)。

当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数。

overload重载只是一种语言特性,与继承无关、与多态无关、与面向对象无关。

main函数不能被重载。

举例:

void print(const string &);
void print(double);
void print(int);

如果两个函数的函数名、形参列表 都相同,只有二者返回值类型不同,这种情况是不被允许的,第二个函数的声明将会是错误的:

int lookup(const Account& );
bool lookup(const Account& );	//错误:与上一个函数相比只有返回值类型不同

函数的重载不区分函数的返回值类型,即只要 函数名相同、函数形参列表不同,就可以构成函数重载,与函数的返回值类型是否相同无关:

void func(int a) {}
int func(int a, int b) {}
//正确
18.1.1 C语言不支持overload函数重载:

overload重载、 overwrite重写、 override覆盖 这些都是C++语言的特性,C语言连最简单的重载都是不被允许的,因为C语言中不支持 name mangling,所以不允许函数同名,做不到重载。

所谓的“name mangling”,目的就是为了给重载的函数不同的签名,以避免调用时的二义性调用基本支持函数重载的语言都需要进行name mangling。

大体的做法就是在编译时,在函数名的基础上再附加参数名、类型等额外的信息,用以区别源程序中同名的函数,例如:

double sum(double x, double y) {
}
void foo(int lhs, int rhs) {
}

------
编译后:
00000022 T __Z3fooii
00000000 T __Z3sumdd

18.2 overwrite 重写:

重写的本质是子类成员函数屏蔽了父类成员函数。

是指派生类的函数 屏蔽了 与其同名的基类函数,规则如下:

  1. 如果派生类的函数与基类的函数 同名但形参列表不同,则不论有无virtual关键字,基类的函数都将被屏蔽;
  2. 如果派生类的函数与基类的函数 同名且形参列表相同,此时:
    ① 如果基类中的函数没有virtual关键字,则基类函数将被屏蔽;
    ② 如果基类中的函数有virtual关键字,且二者的函数返回值类型相同,则发生override覆盖;
    ③ 如果基类中的函数有virtual关键字,但二者的函数返回值类型不同,则编译出错。

举例:

class Base {
public:
    void f1(int);
    virtual void f2(int);

	void f3(int);
	virtual void f4(int);
};

class Derived : public Base {
public:
	void f1(int, int);			//重写f1
	virtual void f2(int, int);	//重写f2

	void f3(int);				//重写f3
	virtual void f4(int);		//override覆盖f4
};

18.3 override 覆盖:

override关键字 的真正作用:
显式的声明派生类的虚函数的目的是覆盖基类中的某个同名虚函数,避免笔误等原因造成覆盖失败,导致程序出错。

  对于虚函数的覆盖,正确的写法是在派生类中声明一个与基类中的虚函数 函数名字、形参列表 都完全相同的函数。但是!派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为。编译器将认为新定义的这个函数与基类中原有的函数是相互独立的。这是,派生类的函数并没有覆盖掉基类中的版本。
  就实际的编程习惯而言,这种声明往往意味着发生了错误,因为我们原本希望派生类能覆盖掉基类中的虚函数,但是一步小心把形参列表弄错了。
  要想调试这样的错误非常困难,C++11标准中我们可以使用override关键字来说明派生类的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错。

举例:

class B {
public:
	virtual void f1(int) const;
	virtual void f2();
	void f3();
};

class D : public B {
public:
	void f1(int) const override;	//正确:f1与基类中的f1匹配
	void f2(int) override;			//错误:B没有形如f2(int)的函数
	void f3() override;				//错误:f3不是虚函数
	void f4() override;				//错误:B没有名为f4的函数
};

18.3.1 引申:派生类与基类中的虚函数的返回值类型必须相同:

#include <iostream>
using namespace std;

class B {
public:
	virtual void func(void) {}
};

class D : public B {
public:
	virtual int func(void) { return 1; }
};

int main() {
	return 0;
}

上面的例子中,func函数在派生类与基类中的函数名、形参列表相同,构成了虚函数覆盖的条件,但二者的返回值类型不同,此时编译将报错:

error: virtual function 'func' has a different return type ('int')
      than the function it overrides (which has return type 'void')
        virtual int func(void) {

如果func()函数前不加virtual限制(此时func是一个普通函数,而不是虚函数),则编译可以通过,此时发生的是函数的重写(overwrite),而不是覆盖(override)。

18.3.2 引申:关键字 final

有时候对于某个类,我们不希望其他类继承它,或者不想考虑它是否适合作为一个基类。
C++11标准中提供了一个关键字:final,用于防止继承的发生。

class NoDerived final {

};		//NoDerived不能作为基类,其他类不能继承它


class Base {

};

class Last final : public Base {

};		//Last继承自Base,但它不允许别人继承自己


19. 默认参数:

默认参数指的是当函数调用中省略了实参时自动使用的一个值。

C语言中没有参数默认值的概念,可以利用宏来模拟参数默认值;
在C++中,可以为参数指定默认值。在函数调用时没有指定与形参相对应的实参时,就自动使用默认参数。

默认参数的语法与使用:

  1. 默认参数只可在函数声明中设定一次。只有在函数 无 声明时,才可以在函数定义中设定;
  2. 默认参数定义的顺序为 自右向左。即 如果一个参数设定了缺省值时,其右边的参数都要有缺省值
  3. 在调用带有默认参数的函数时,传入的实参与形参结合的顺序是 自左向右
  4. 默认值可以是全局变量、全局常量、甚至是一个函数,但不可以是局部变量。 因为默认参数的调用是在编译时确定的,而局部变量位置与默认值在编译时无法确定。

举例:

int mal(int a, int b, int c = 6, int d = 8, int e = 10);		//默认参数定义的顺序是自右向左,第一个参数c定义了缺省值,则c右边的所有参数d、e都必须定义缺省值

int main() {
    mal(1, 2, 3);		//等价于: mal(1, 2, ,3 8, 10);
    					//函数调用时,实参与形参结合的顺序是 自左向右
    					
}

20. malloc\calloc\realloc 函数的区别:

函数原型:

#include <stdlib.h>

void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);

void free(void *ptr);

20.1 malloc:

malloc用于申请size大小的堆内存,并返回指向这块内存的指针;
使用malloc申请的内存未被初始化,必须由用户使用memset对这块内存进行初始化;
malloc如果申请内存失败,则返回一个空指针;

20.2 calloc:

calloc函数用于申请 nmemb 个 大小为size字节的连续内存(总大小为 (nmemb*size) byte),
使用calloc函数申请的内存是已经被系统初始化为0的;
calloc适合为数组申请内存。

20.3 realloc:

realloc函数将参数ptr所指向的内存的大小调整为size字节,原来的内存中的内容将被保留到新申请的内存中;
如果新内存 < 旧内存,则 旧内存中多余的数据将被截断丢弃;
如果新内存 > 旧内存,则 新内存中多余的部分将是未被初始化的
realloc传入的参数
ptr指针必须是之前调用malloc、calloc、realloc返回的指针;

20.4 free:

free用户释放一块堆内存,传入的指针参数ptr必须是之前使用 malloc、calloc、realloc申请的内存;
对一个块内存调用free释放两次的行为是未定义的(大部分程序的结果是core dump,理由是已经归还系统的内存可能已经被分配给其他程序使用,本程序再次调用free可能会释放一块正在被他人使用的内存,系统core dump加以防护);
对null空指针调用free是允许的。


21. 如何判断迭代器为空:

在C++中,迭代器是类,虽然是封装了指针的类,但是绝对不能直接拿来当指针用,类似 if(iter) 或者 if(iter == nullptr) 的写法都会导致编译出错。

当需要判断查找的结果时,正确的写法是判断find的返回值是否等于 尾后迭代器(end())

例如:

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        set<int> s_;

        for(auto it = nums.begin(); it != nums.end(); ++it) {
            if(s_.find(*it) != s_.end())    //判断set中是否存在某元素,切记迭代器不能直接与NULL进行比较!
                return *it;
            s_.insert(*it);
        }
        return -1;
    }
};

22. 虚函数表的内存结构:

虚函数表的第一项是什么?第二项是什么?

virtual table 的第一个slot 存储的是 type_info object; 的地址(一个 class type_info 类型的对象object),用于支持RTTI(Runtime Type Identification,运行时类型识别),在执行期获取class object的内部实现是这样的:

((type_info*)(pt->vptr[0]))->_type_descriptor;
//获取类的虚函数表中的slot 1上的内容(pt->vptr[0]),
//然后将其强转成 (type_info*) 类类型的指针,
//最后获取 type_info对象中的数据成员 _type_descriptor,即对应pt所指类的类型

virtual table 的第二个slot 存储的是 destructor 指向类的虚析构函数指针。

从virtual table 的第三个slot 开始存储的才是类中的普通的 virtual虚函数指针。


23. 写一个C程序辨别系统是大端or小端字节序:

23.1 大端字节序 & 小端字节序:

大小端字节序,指的是数据在内存中的存储方式。

大端模式(Big-Endian): 数据的高字节 保存在 内存的低地址中,数据的低字节保存在 内存的高地址中;

小端模式(Little-Endian): 数据的的高字节 保存在 内存的高地址中,数据的低字节 保存在 内存的低地址中。

unsigned int value = 0x123456; 为例,其分别在两种字节序下的存储情况是:

在这里插入图片描述

23.2 网络序 & 主机序:

网络序:
TCP/IP协议规定,网络数据流采用大端字节序。

主机序:
与CPU有关,X86架构的CPU全部是小端字节序,ARM架构的CPU也是小端字节序。

所以,对于X86、ARM的主机,当向网络中发、收数据时,必须要先进行字节序转换。

写一个C程序辨别系统的大小端:

  1. 方法一:

使用union联合体:

#include <stdio.h>
  
typedef union Demo {
    unsigned short value;
    char array[2];
} Demo;


int main() {

    Demo d;
    d.value = 0x1234;

    if(d.array[0] == 0x12 && d.array[1]) {
        printf("big endian\n"); //高地址存高位字节
    }
    else {
        printf("little endian\n");
    }
    return 0;
}

------

little endian

  1. 方法二:
#include <stdio.h>
  
int main() {
    int num = 0x12345678;
    char *ptr = (char*)&num;

    if(*ptr == 0x78) { //低地址存高位
        printf("little endian\n");
    }
    else {
        printf("big endian\n");
    }
    return 0;
}

------

little endian

扩展:

23.3 malloc/calloc/realloc:

C语言中用于分配内存的函数:malloc/calloc/realloc 之间的区别:

#include <stdlib.h>

void* malloc(size_t size);
void* calloc(size_t count, size_t size);
void* realloc(void* ptr, size_t size)

------

char* ptr = (char*)malloc(sizeof(char) * 2);
char* ptr = (char*)calloc(2, sizeof(char));
char* p = (char*)realloc(ptr, 3*sizeof(char));

三者都是从堆上申请内存,成功则返回指向所分配的堆内存的指针,失败则返回NULL。

区别在于:
(1)malloc申请的内存是没有被初始化的,申请后必须再通过memset进行初始化;
(2)calloc申请的内存是已经被初始化为0的,从堆上申请 nsize 字节的空间,适合为数组申请空间;
(3)realloc比较特殊,它重新分配参数
ptr所指的空间为n个字节,如果新申请的大小n 不大于(<=)原有空间的大小,则保持不变;若n大于(>)原有空间的大小,则重新分配堆上的一块内存,并复制原有内容到新分配的堆上的存储空间。

注意无论使用哪种申请堆内存的方式,最后都要使用free()函数释放内存。

23.4 32位与64位系统指的是什么:

通常所说的 32位/64位,指的是CPU GPRS(General Purpose Registers,通用寄存器)的数据宽度。64位指令集就是运算64位数据的指令,也就是说处理器一次可以运算64 bit数据。

从32位到64位使寻址范围、最大内存容量、数据传输、处理速度、数值精度等指标都成倍的增加,带来的结果就是CPU的处理能力得到大幅提升。

在32位和64位机器上各数据类型长度的对比:

32位平台下:
请添加图片描述

64位平台下:

请添加图片描述

注意在32位和64位平台下,char型长度都是1,short型长度都是2,int型长度都是4,long型从4变为8,指针类型长度从4变为8。


24. 写一个C程序辨别系统是16位还是32位:

  1. 方法一:指针位数:
#include <stdio.h>

int main() {
    int *ptr;
    printf("sizeof(ptr) = %u\n", sizeof(ptr));
    return 0;
}

------

如果是32位系统,则sizeof(ptr) = 4
如果是16位系统,则sizeof(ptr) = 2
  1. 方法二:int型的bit位数:
#include <stdio.h>

int main() {
    int i = 32768;
    printf("i = %d\n", i);
    return 0;
}

------

如果i的值为-32768,则是16位系统
如果i的值是32768,则是32位系统

根据int型的bit位数进行判断的衍生方法还有很多:

#include <stdio.h>

int main() {
    int i = 65536;
    printf("i = %d\n", i);	//16位系统输出0, 32位系统输出65536

    int j = 65535;
    printf("i = %d\n", j);	//16位系统输出-1, 32位系统输出65535
    
    return 0;
}

24.1 C++语言的数据类型:

在这里插入图片描述

24.2 原码、反码、补码:

反码:原码按位取反
补码:反码+1,也就是 原码的反码再加一

正数的反码是其本身,正数的补码是其本身;
负数的补码是其原码取反加一。

在系统中的数组都是以 补码 的形式存放。

以16 bit整数为例(32位系统下的short型),如果是有符号数(signed short),表示的数值范围是 (-215 ~ 215 - 1 ),即 ( -32768 ~ +32767);如果是无符号数(unsigned short),表示的数值范围是 (0 ~ 216 - 1),即 (0 ~ 65535)。

为什么有 - 32768:
以 4bit位为例:
24 = 16,即4bit为的整数一共可以表示16个数字,按理说应该整数负数各占一半,但是数字0非正非负,这就造成正数负数的比例不是1:1,C语言选择负数多一个:

0000 表示0
0111 表示 +7,为正数的最大值
1111 表示 -7,(最高一位是符号为,表示负数)

1000:表示 -8

所以 4bit位的整数的取值范围是 (-24 ~ 24 - 1)。


25. i++ 是否是原子操作:

不是,i++的实现步骤简单描述如下:
(1)读取i到某个寄存器X;
(2)X++;
(3)将更新后的X的内容存回i。

只有单条指令的操作才是原子的(Atomic),因为无论如何单条指令的执行是不会被打断的。
但是自增操作(i++)在编译为 汇编代码 之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了,去执行别的代码了。


26. C语言中的指数运算如何实现:

C语言中的指数运算需要用到 pow() 函数,例如:2的4次方的写法是:

#include <math.h>		//注意头文件
pow(2, 4);

27. new/delete和malloc/free的区别与底层实现:

  1. malloc和free是C/C++语言的 “标准库函数”,new/delete是C++的 “运算符”。它们都可以用于申请/释放动态内存(堆内存);
  2. new返回指定类型的指针,并且可以自动计算所需大小;malloc返回的是(void*)类型的指针,返回后需要强制转换为实际类型的指针,并且需要程序员计算所需字节数;
  3. new/delete在对象创建/销毁时可以自动调用对象的构造/析构函数,malloc/free则不能,malloc申请或的内存是一块新内存,其值是随机的,需要另外调用memset对其进行初始化;
  4. new/delete底层的实现是对malloc/free的封装,而malloc/free的底层也不是直接操作内存,而是对系统调用API的封装;
  5. new[]、delete[] 会额外多开辟4个字节,用于存储数组中的对象个数。

28. struct结构体对齐:

struct结构体“字节对齐”到底是怎样的一种“对齐”?

以结构体成员中占用内存最多的数据类型所占的字节数为标准,所有的成员在分配内存时都要与这个长度对齐。

填充规则: 什么时候另起一行?什么时候紧接着填充?

理论上所有成员在分配时都是紧接在前一个变量后面依次填充的,但是如果是“以N对齐”为原则,那么当一行中剩下的空间不足以填充某成员变量时,则该成员变量在分配内存时另起一行分配。

举例:(32位机器中)

struct {
	char 	a;
	double	b;
	int		c;
} demo;

sizeof(demo) = 24;
//sizeof(char) = 1; sizeof(double) = 8; sizeof(int) = 4;

29. 谈谈Unix系统调用与库函数的关系:

从一道小题开始:

下列哪个操作可以不需要在内核态执行?
A. 系统调用
B. malloc/free
C. 软中断
D. 内存换页

答案:B。 malloc/free是库函数。


30. memcpy函数的实现:


31. const的底层实现:


32. C++11中引入了哪些新特性:


33. C++多态/动态绑定是如何实现的:

C++中的“多态”,实际修饰的是“基类类型的指针或引用” ---- 使“基类的指针&引用”具有“多种形态”,即当使用基类类型的指针分别调用基类类型或派生类型对象中的虚函数时,可以产生不同的效果,目的是为不同的数据类型实体提供统一的接口,实现同一份代码的复用。

具体实现上,在编译阶段,当编译器发现某个class类中含有virtual虚函数时,① 首先,编译器会为这个类构建一个虚函数表,虚函数表,虚函数表中存储这个类中的每一个虚函数地址指针;② 其次,编译器会在每个含有虚函数的类中插入一个隐式的数据成员,即vptr虚函数表指针,在类对象初始化时,由类的构造函数负责将vptr初始化为虚函数表地址。

派生类中的虚函数可能有三种版本:① 直接继承基类的虚函数版本; ② 重新改写覆盖基类的版本;③ 纯虚函数。

在编译阶段,当通过基类指针&引用调用派生类中的虚函数时,编译器可以通过派生类对象中的vptr虚函数表地址找到对应的派生类虚函数表,以及虚函数地址指针,完成函数的调用,但是此时派生类中的虚函数是三个版本中的哪一个,在编译阶段无法确定,只能在程序运行时确定,这就是运行时多态。


34. struct和union的区别:

  1. 在存储多成员信息时,编译器会为struct中的每个成员都分配内存空间,union每个成员共用一个存储空间;

35. #define和const的区别:

  1. 从定义“常量”的角度:
    const定义的常数也是 变量也带类型,#define定义的只是个常数不带类型
#define  CYCLE    5
const  int  cycle = 5; 
  1. 从起作用的阶段:
    #define在**“预编译”阶段起作用,而const在“编译、运行”**的时候起作用;
  2. 从起作用的方式:
    #define只是简单的字符串替换,没有类型检查。 而const有对应的数据类型,要进行判断,可以避免一些低级的错误。
  3. 从空间占用:
    #define在预处理后占用“代码段”空间,const常量占用“数据段”空间。
    #define如果在多个地方被调用,就会在多个地方被展开,这会导致代码段占用的空间变大。
#define    PI   3.14	//预处理后,占用代码段空间
const float PI = 3.14;	//占用数据段空间(局部变量占用栈,全局变量占用静态区)
  1. 从代码调试方便程度:
    因为#define发生在预处理阶段,所有是不能被调试的,const发生在编译和运行阶段,是可以被调试的。
  2. 从是否可以再定义的角度:
    const不能重定义,#define可以通过 #undef 取消某个符号的定义后再重新定义。
  3. 从某些特殊功能:
    #define可以用于防止头文件重复引用,const则不能:#ifndef
  4. 从某些复杂功能的实现的角度:

36. C++的const的实现原理:

当const用于修饰函数时,分两种情况:

  1. 被修饰的变量是基本类型:
    例如const int,此时,程序在编译时,就将变量用常量来替换了,替换后再进行编译;
  2. 被修饰的变量是非基本类型:
    例如 const struct,此时,程序在编译时不知道该用什么值来替换,所以,将会用一块内存地址来替换,然后再编译。

const用于修饰函数也包括两种情况:

  1. const在函数名前:
    此时表示 函数的返回值 是一个const类型;
  2. const在函数名后:
    此时只用于类的成员函数,const修饰的是成员函数的this指针,表示其是一个常量指针,不会修改对象实例中的值

遗留:const修饰指针的实现原理是什么?


37. 常见的设计模式有哪些?

单例模式:
工厂模式:
适配器模式:
观察者模式:
装饰器模式:
策略模式:


38. 使用多态实现的设计模式有哪些?


二、操作系统:

1. 进程的地址空间:

以32位操作系统为例:

请添加图片描述

在32位的操作系统中,地址总线是32位,最大寻址能力是 232 = 4G,所以进程的虚拟地址空间的大小是 4G,即 0x00000000 到 0xffffffff。

其中,内核空间占用 1G,Linux默认情况下会将高地址的 1GB空间分配给内核,即 0xc0000000 到 0xffffffff。 用户使用的空间为3G,即 0x00000000 到 0xc0000000,其中又分为:栈空间、堆空间、静态区。
静态区又可细分为:读写段(全局变量、静态局部变量)、只读段(常量区) 和 代码段。

常用表示内存字节数的十进制/十六进制换算:

2^10 = 1024                = 1K = 0x        400
2^20 = 1024*1024           = 1M = 0x    10 0000
2^30 = 1024*1024*1024      = 1G = 0x  4000 0000
2^32 = 1024*1024*1024*1024 = 4G = 0x1 0000 0000


2. 一个进程最多可以创建多少个线程:

请添加图片描述

对于32位系统,一个进程的虚拟地址空间大小为4G,其中高地址的1G留给内核使用,用户态的虚拟地址空间大小为3G,Linux下的线程默认的栈空间大小为 8M(可以手动修改大小),那么一个进程最多能创建 300个左右的线程;

对于64位系统,用户态的虚拟地址空间大小为 128T(内核态的虚拟地址空间大小也是128T,其余空间未定义)。如果只按照虚拟内存大小计算,那么将可以创建 (128T/8M = 1000万+) 个线程,但实际上除了虚拟内存的限制,还有系统的限制。

例如如下三个内核参数,都会影响创建线程的上限:

/proc/sys/kernel/threads-max	//表示系统支持的最大线程数,默认值 14553
/proc/sys/kernel/pid_max		//表示系统全局的PID号数值的限制,默认值 32768,当进程或线程PID超过这个数值时将会创建失败
/proc/sys/vm/max_map_count		//表示一个进程可以用于的VMA(虚拟内存区域)的数量,默认值 65530

查看Linux系统中线程的栈空间默认大小:
在这里插入图片描述

8192 Kbytes = 8M 字节。


3. 同一主机上有两个进程,其中各有一个变量a和b。请问a和b的地址可能相同吗?请详细说明原因。

虚拟地址可能相同,物理地址不可能相同。


4. 死锁产生的条件,以及如何避免死锁,银行家算法,产生死锁后如何解决?


5. 什么是内存换页?Linux分页原理?Linux虚拟内存的实现原理?


6. 什么是Segment Fault?产生Segment Fault的原因有哪些?

6.1 什么是段错误:

Segment Fault直译为“段错误”,指的是程序访问的内存超过了系统分配给这个进程的内存空间。

实现上,每个进程所拥有的内存空间的大小由一个 GDTR 48位寄存器保存,一旦一个程序发生了越界访问,CPU就会产生相应的异常保护,于是segment fault就出现了。

在发生段错误后,系统会向进程发送 SIGSEGV 信号。

6.2 可能会造成段错误的原因:

段错误就是访问了不可访问的内存,这个内存要么是不存在的,要么是系统保护的。

  1. 内存访问越界:
    a. 由于使用错误的数组下标,导致数组访问越界;(这个不是一定的,要超出进程的堆或栈的内存范围才会发生段错误)
    b. 搜索字符串时依靠’\0’来判断字符串结束,导致一直读,读到了非法地址;
    c. 使用strcpy、strcat、strcmp等字符串操作函数,将目标字符串读/写爆,应该使用strncpy、strncat、strncmp等函数防止越界;
  2. 多线程程序使用了线程不安全的函数:
  3. 多线程读写的数据未加锁保护:
    对于多线程同时访问的全局数据,应该注意加锁保护,否则很容易造成段错误;
  4. 非法指针:
    a. 使用NULL空指针;
    b. 使用未经初始的指针;
    b. 随意进行指针类型转换,导致转换后的结构体不对齐,引起段错误;
  5. 堆栈溢出:
    不要使用过大的局部变量(局部变量分配栈上),函数嵌套深度不要太大,避免造成栈溢出。

6.3 如何调试段错误:

在C/C++程序中,内存管理的大部分工作都是要程序员来完成的。内存管理是一个比较繁琐的工作,无论你多高明,经验多丰富,难免也会犯一些小错误。

使用gdb可以快速的定位一些“段错误”的语句,而对于大型程序,如何跟踪并找到程序中的错误位置就是需要掌握的一门技巧。

  1. 在程序内部的关键部位输出printf打印信息,以便于跟踪段错误在代码中的可能位置;
  2. 使用gdb调试,通过-g编译,gdb运行程序,在运行到段错误的地方,会自动停下来显示出错的行和行号。

"Program received signal SIGSEV, Segmentattion fault"
ing main() at seg.c:7

请添加图片描述

  1. 使用gdb调试发生segment fault后的core dump文件。

7. 什么是线程同步?为什么需要线程同步?

7.1 什么是线程同步:

“线程同步”指的是在多线程的环境下,有些资源可能被多线程同时访问,当有一个线程对这段内存地址进行操作时,其他线程都不可以对这段内存进行操作,直到该线程完成操作。

此时其他线程都处于 等待 状态(进程三态Wait)。

7.2 Linux如何实现线程同步:

实现线程同步的方法有很多,临界区对象就是其中一种。

线程同步方法包括:
mutex() / wait() / signal() / notify() / notifyAll();

pthread_cond_signal() / pthread_cond_broadcast

7.3 名词解释:

  1. 临界区:(critical section)
    所谓“临界区”就是代码的一个区间,如果有两个及以上的线程同时执行有可能出现问题,需要加互斥锁进行保护。
  2. 自旋锁:(spinlock)
    “自旋锁”是一种互斥锁的实现方式,相比于一般的互斥锁会在等待期间 放弃CPU(进程进入到“等待”态),自旋锁则是不断循环并测试锁的状态,这样一直占有CPU。
    互斥锁与自旋锁的根本区别就在于:当尝试加锁无法获得锁时,是让出CPU还是一直占据CPU。

8. 虚拟内存的作用是什么:

  计算机系统的各种内存管理策略,例如“分页”、“分段”等,所有这些策略都有相同的目标,就是同时将多个进程保存在内存中,以便允许多道程序。然而,这些策略都倾向于要求每个进程在执行之前应完全处于内存中(然而这是不切实际的,内存的大小是很有限的)。

  “虚拟内存技术”允许执行进程不必完全处于内存中。 ------ 根本作用
这方案的一个主要优点就是:程序可以大于物理内存。 此外,虚拟内存将内存抽象成一个巨大的、同一的存储数组,进而实现了用户看到的逻辑内存与物理内存的分离。这种技术使得程序员不再担忧内存容量的限制。

  虚拟内存还允许进程轻松共享文件和实现共享内存。 ------ 次要作用
此外,它为创建进程提供了有效的机制。然而,虚拟内存的实现并不容易,并且使用不当还可能会大大降低性能。

http://c.biancheng.net/view/1270.html


三、网络编程:

1. 请描述IO多路复用机制:

  1. Unix下的IO网络模型有5种:阻塞式IO、非阻塞式IO、IO多路复用、信号驱动式IO、异步IO。
  2. IO多路复用技术是通过把多个IO的阻塞 复用到同一个select/poll/epoll等系统调用的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端连接上的IO请求。
  3. 与传统的多线程/多进程模型相比,IO多路复用的最大优势是系统开销小,不需要创建新的额外的进程或者线程。
  4. 目前支持IO多路复用的系统调用有:select/poll/epoll。相比于select,epoll做了重大改进:
    ① epoll支持一个进程内打开的套接字句柄个数不受限制,而select最大打开句柄数为1024(select使用位域的方式传递文件描述符,Linux下位域的最大长度是1024);
    ② IO效率不会随着FD数目的增加而线性下降;
    ③ epoll的API更简单。

2. 浏览器输入www.baidu.com后的完整过程,越详细越好:

  1. 浏览器获取域名www.baidu.com;
  2. 浏览器应用程序向DNS服务器请求www.baidu.com的IP地址;
  3. DNS解析得到www.baidu.com的IP地址并将其返回给浏览器应用程序;
  4. 浏览器与服务器建立TCP连接(默认端口80);
  5. 浏览器与服务器完成TLS四次握手建立安全的HTTPS连接;
  6. 浏览器向服务器发出HTTP请求,请求百度首页;
  7. 服务器解析浏览器的HTTP请求后,返回HTTP响应给浏览器;
  8. 如果不是HTTP长连接,则释放TCP连接;
  9. 浏览器解析HTTP响应内容,并展示web页面。

3. 描述长连接与短连接:

  1. http的长连接、短连接,实质上就是tcp的长连接、短连接;
  2. HTTP的 Keep-Alive,是应用层(用户态)实现的,称为“HTTP长连接”;
    TCP的Keepalive,是TCP层(内核态)实现的,称为“TCP保活机制”;
  3. HTTP/1.0 中,模式使用的是短连接;从HTTP/1.1 开始,默认使用的是长连接;
  4. 使用长连接的HTTP协议,会在请求头中加入这行:Connection: keep-alive,HTTP响应头中也会加入 Connection: keep-alive,这样才算建立起了HTTP长连接;
  5. HTTP长连接并不是一直保持连接,web服务软件一般会提供keepalive_timeout参数,用来指定HTTP长连接的超时时间,超时时间内未收到对端数据则会释放连接;
  6. TCP保活机制会在一条TCP连接上周期性的发送探测报文,若链路正常时则会收到对端的响应,以此来重置TCP保活时间,这一些列动作都是由内核来自动完成;
  7. 如果要使用TCP保活机制,则需要通过socket选项 SO_KEEPAIVE 显式开启,默认下TCP保活机制是关闭的,也就是说此时TCP将无法主动发现一条连接是否出现异常。 TCP的保活时间超时2小时。

4. Session、Cookie、Cache 的区别?

https://mp.weixin.qq.com/s/wB00GWeo66rSyLDgA-MKfw


5. 如果有几千个session,怎么提高效率?


6. session是存储在什么地方,以什么形式存储的?


7. Linux中,如何查看端口12345被哪个程序占用?

方法一:netstat

netstat -anp | grep 12345

netstat命令用于查看网络连接和端口状态。

方法二:lsof

lsof -i:12345

lsof含义为“列出打开文件”(lists of open files),而在Unix中由于一切设备皆文件,所以losf这个命令格外的强大,支持众多的选项。其中用于查看网络端口的命令是 lsof -i:端口号


8. HTTP中GET与POST的区别:

8.1 GET与POST是什么:

GET方法的含义是请求从服务器获取资源,这个资源可以是静态的文本、页面、图片视频等,例如:

//请求:
GET /index.html HTTP/1.1
Host: www.coding.com

//响应:
返回 index.html 的页面资源

而POST方法则相反的,它向URI指定的资源提交数据,数据就放在 body 里,例如:

//请求:
POST /index.html HTTP/1.1
Host: www.coding.com

//响应:
返回服务器处理的结果

8.2 GET与POST的区别:

GET方法是安全且幂等的,POST不是安全且不是幂等的。

因为GET方法是“只读”操作,POST是“新增或提交数据”的操作,会修改服务器上的资源。

安全和幂等的概念:

  1. 在HTTP协议中,所谓的“安全”是指请求方法不会“破坏”服务器上的资源;
  2. 所谓的“幂等”,意思是多次执行相同的操作,结果都是“相同”的。

9. HTTP/1.0、HTTP/1.1、HTTP/2的演进:

9.1 HTTP/1.1 相比于 HTTP/1.0 的改变:

  1. 使用 TCP 长连接 的方式改善了 HTTP/1.0 短连接造成的性能开销。
  2. 支持 管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

9.2 HTTP/2 相比于 HTTP/1.1 的改变:

  1. 头部压缩;
  2. 二进制格式;
  3. 数据流;
  4. 多路复用;
  5. 服务器推送。

https://www.cnblogs.com/xiaolincoding/p/12442435.htm


四、数据库:

1. 实践中如何优化MySQL:

四条,影响程度依次递减:

  1. SQL语句及索引优化;
  2. 数据库表结构优化;
  3. 系统配置优化;
  4. 硬件优化。

2. 描述MySQL主从同步的过程:


3. Redis中的主要存储类型及数据结构:


4. 如何实现MySQL与Redis数据库双写时的数据一致性:

业界普遍的做法是:
两阶段提交。


五、系统设计:

1. 如何设计一个高并发系统:

  1. 数据库的优化,包括合理的事务隔离级别、SQL语句优化、索引优化;(因为数据库涉及到磁盘的IO读写,所以一般会最容易称为整个系统的性能瓶颈,因此对于并发度的优化一般也是要从数据库开始)
  2. 合理使用缓存(多级缓存),尽量减少数据库IO;
  3. 分布式数据库、分布式缓存;
  4. 服务器的负载均衡。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值