C/C++重点八股文-创新互联

1.C/C++关键字 1.1 static(静态)变量 在C中,关键字static是静态变量:
  • 静态变量只会初始化一次,然后在这函数被调用过程中值不变。
  • 在文件内定义静态变量(函数外),作用域是当前文件,该变量可以被文件内所有函数访问,不能被其他文件函数访问。为本地的全局变量,只初始化一次。
在C++中,类内数据成员可以定义为static
  • 对于非静态数据成员,每个对象有一个副本。而静态数据成员是类的成员,只存在一个副本,被所有对象共享。
  • 静态成员变量没有实例化对象也可以使用,“类名:静态成员变量”
  • 静态成员变量初始化在类外,但是private和protected修饰的静态成员不能类外访问。
class Stu
{public:
	static int age;
private:
	static int height;
};
//初始化静态成员变量
int Stu::age = 19;
int Stu::height = 180;

int main()
{cout<
  • 在类中,static修饰的函数是静态成员函数。静态成员函数一样属于类,不属于对象,被对象共享。静态成员函数没有this指针,不能访问非静态的函数和变量,只能访问静态的。
与全局变量相比,静态数据成员的优势:
  • 全局变量作用域是整个工程,而static作用域是当前文件,避免命名冲突
  • 静态数据成员可以是private成员,而全局变量不能,实现信息隐藏
为什么静态成员变量不能在类内初始化?

因为类的声明可能会在多处引用,每次引用都会初始化一次,分配一次空间。这和静态变量只能初始化一次,只有一个副本冲突,因此静态成员变量只能类外初始化。

创新互联成立于2013年,我们提供高端重庆网站建设公司成都网站制作成都网站设计公司、网站定制、成都营销网站建设微信平台小程序开发、微信公众号开发、营销推广服务,提供专业营销思路、内容策划、视觉设计、程序开发来完成项目落地,为人造雾企业提供源源不断的流量和订单咨询。为什么static静态变量只能初始化一次?

所有变量都只初始化一次。但是静态变量在全局区(静态区),而自动变量在栈区。静态变量生命周期和程序一样,只创建初始化一次就一直存在,不会销毁。而自动变量生命周期和函数一样,函数调用就进行创建初始化,函数结束就销毁,所以每一次调用函数就初始化一次。

在头文件中定义静态变量是否可行?

不可行,在头文件中定义的一个static变量,对于包含该头文件的所有源文件,实质上在每个源文件内定义了一个同名的static变量。造成资源浪费,可能引起bug

静态变量什么时候初始化
  • 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。

  • 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。

  • 在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

  • 而在C++中,初始化时在执行相关代码时才会进行初始化,C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

1.2 const的作用

常量类型也称为const类型,使用const修饰变量或者对象

在C中,const的作用为:
  • 定义变量(局部或者全局)为常量
const int a = 10; //常量定义时,必须初始化
  • 修饰函数的参数,函数体内不能修改这个参数的值
  • 修饰函数的返回值
    • const修饰的返回值类型为指针,返回的指针不能被修改,而且只能符给被const修饰的指针
      const char* GetString()
      {//...
      }
      
      int main()
      {char *str = GetString();//错误,str没被const修饰
      	const char *str = GetString();//正确
      }
      
    • const修饰的返回值类型为引用,那么函数调用表达式不能做左值(函数不能被赋值)
      const int & add(int &a , int &b)
      {//..
      }
      int main()
      {add(a,b) = 4;//错误,const修饰add的返回引用,不能做左值
      }
    • const修饰的返回值类型为普通变量,由于返回是普通临时变量,const修饰没意义。
在c++中,const还有作用为:
  • const修饰类内的数据成员。表示这个数据成员在某个对象的生命周期是常量,不同对象的值可以不一样,因此const成员函数不能在类内初始化。
  • const修饰类内的成员函数。那么这个函数就不能修改对象的成员变量
const的优点?
  1. 进行类型检查,使编译器对处理内容有更多了解。

  2. 避免意义模糊的数字出现,类似宏定义,方便对参数进行修改。

  3. 保护被修饰的内容,防止被意外修改

  4. 为函数重载提供参考

    class A
    {void f(int i){...} //非const对象调用
    	void f(int i) const {...}//const对象调用
    }

5.节省内存
6.提高程序效率(编译器不为普通const常量分配存储空间,而保存在符号表中。称为一个编译期间的常量,没有存储和读内存的操作)

什么时候使用const?
  • 修饰一般常量

  • 修饰对象

  • 修饰常指针

    const int *p;
    int const *p;
    int *const p;
    const int *const p;
  • 修饰常引用

  • 修饰函数的参数

  • 修饰函数返回值

  • 修饰类的成员函数

  • 修饰另一文件中引用的变量

    extern const int j;
const和指针(常量指针、指针常量)
  • 常量指针(const 修饰常量,const在*的左边)

    const int *p = &a; // const修饰int,指针的指向可以修改,但是指针指向的值不能改
    int const *p;//同上
    p = &b;//正确
    *p = 10;//错误
  • 指针常量(const修饰指针,const在*的右边)

    int *const p = &a;//const修饰指针,指针的指向不可以改,但是指针指向的值可以改
    *p = 10;//正确
    p = &b;//错误
  • const都修饰指针和常量(指针和常量都不能修改)

    const int *const p;
    int const *const p;
顶层const和底层const
  • 顶层const(常量指针):指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是 * 号的右边
  • 底层const(指针常量):指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是 * 号的左边
const和static的作用

static

  • 不考虑类的情况
    • 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
    • 默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
    • 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
  • 考虑类的情况
    • static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
    • static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问

const

  • 不考虑类的情况

    • const常量在定义时必须初始化,之后无法更改

    • const形参可以接收const和非const类型的实参,例如// i 可以是 int 型或者 const int 型void fun(const int& i){ //…}

  • 考虑类的情况

    • const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
    • const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值

补充一点const相关:const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突

1.3 switch语句中case结尾是否必须加break

**一般必须在case结尾加break。**因为通过switch确认入口点,一直往下执行,直到遇见break。否则会执行完这个case后执行后面的case,default也会执行。 注,switch(c),c可以是int、long、char等,但是不能是float

1.4 volatile 的作用

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。

  • 编译器不再进行优化,从而可以提供对特殊地址的稳定访问。
  • 系统总是重新从它所在的内存读取数据,不会利用cache中原有的数值。
  • 用于多线程被多个任务共享的变量,或者并行设备的硬件寄存器
1.5 断言ASSERT()是什么?

**是一个调试程序使用的宏。**定义在中,用于判断是否出现非法数据。括号内的值 为false(0),程序报错,终止运行。

ASSERT(n != 0);// n为0的时候程序报错
k = 10/n;

ASSERT()在Debug中有,在Release中被忽略。 ASSERT()是宏,assert()是ANSCI标准中的函数,但是影响程序性能。

1.6 枚举变量的值计算
#includeint main()
{enum {a,b=5,c,d=4,e};
	printf("%d %d %d %d %d",a,b,c,d,e); 
	return 0;
}

输出为 0 5 6 4 5

1.7 字符串存储方式
  1. 字符串存储在栈中
char str1[] = "abc";
char str2[] = "abc";
  1. 字符串存储在常量区
char *str3 = "abc";
char *str4 = "abc";
  1. 字符串存储在堆中
char *str5 = (char*)malloc(4);
strcpy(str5,"abc");
char *str6 = (char*)malloc(4);
strcpy(str6,"abc");
  1. 字符串是否相等
  • str1 != str2 ,str1和str2是两个字符串的首地址。
  • str3 == srt4 , str3和str4是常量的地址,同样字符串在常量区只存在一份。
  • str5 != str6 ,str5 和str6是指向堆的地址。
    在这里插入图片描述
1.8 程序内存分区
内存高地址栈区
堆区
全局/静态区 (.bss段 .date段)
常量区
内存低地址代码区

在这里插入图片描述

  1. 栈区(stack)
  • 临时创建的局部变量存放在栈区。

  • 函数调用时,其入口参数存放在栈区。

  • 函数返回时,其返回值存放在栈区。

  • const定义的局部变量存放在栈区。

  1. 堆区(heap)
  • 堆区用于存放程序运行中被动态分布的内存段,可增可减。

  • malloc函数分布的内存,必须用free进行内存释放,否则会造成内存泄漏。

  1. 全局区(静态区)
  • (c语言中)全局区有.bss段和.data段组成,可读可写。
  • C++不分bss和data
  1. .bss段
  • 未初始化的全局变量存放在.bss段。

  • 初始化为0的全局变量和初始化为0的静态变量存放在.bss段。

  • .bss段不占用可执行文件空间,其内容有操作系统初始化。

  1. .data段
  • 已经初始化的全局变量存放在.data段。

  • 静态变量存放在.data段。

  • .data段占用可执行文件空间,其内容有程序初始化。

  • const定义的全局变量存放在.rodata段。

  1. 常量区
  • 字符串存放在常量区。

  • 常量区的内容不可以被修改。

  1. 代码区
  • 程序执行代码(二进制代码文件)存放在代码区。
1.9 *p++ 和 (*p)++ 的区别
  • *p++ 先完成取地址,然后对指针地址进行++,再取值
  • (*p)++,先完成取值,再对值进行++
1.10 new / delete 与 malloc / free的异同
  • 相同点

    • 都可用于内存的动态申请和释放
  • 不同点

    • new / delete 是C++运算符,malloc / free是C/C++语言标准库函数

    • new自动计算要分配的空间大小,malloc需要手工计算

    • malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

    • new是类型安全的,malloc不是。例如:

      int *p = new float[2]; //编译错误
      int *p = (int*)malloc(2 * sizeof(double));//编译无错误
    • malloc / free需要库文件支持,new / delete不用

    • new是封装了malloc,直接free不会报错,但是这只是释放内存,而不会析构对象

1.11 new和delete是如何实现的?
  • new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的的对象的指针
  • delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
1.12 被free回收的内存是立即返还给操作系统吗?

不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

1.13 C++中几种类型的new
  1. plain new
    言下之意就是普通的new,就是我们常用的new,在C++中定义如下:

    void* operator new(std::size_t) throw(std::bad_alloc);
    void operator delete(void *) throw();

    plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL

    #include#includeusing namespace std;
    int main()
    {try
    	{char *p = new char[10e11];
    		delete p;
    	}
    	catch (const std::bad_alloc &ex)
    	{cout<< ex.what()<< endl;
    	}
    	return 0;
    }
    //执行结果:bad allocation
  2. nothrow new
    nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL,定义如下:

    void * operator new(std::size_t,const std::nothrow_t&) throw();
    void operator delete(void*) throw();
    #include#includeusing namespace std;
    
    int main()
    {char *p = new(nothrow) char[10e11];
    	if (p == NULL) 
    	{cout<< "alloc failed"<< endl;
    	}
    	delete p;
    	return 0;
    }
    //运行结果:alloc failed
  3. placement new
    这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:

    void* operator new(size_t,void*);
    void operator delete(void*,void*);

使用placement new需要注意两点:

  • palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组

  • placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。

    #include#includeusing namespace std;
    class ADT{int i;
    	int j;
    public:
    	ADT(){i = 10;
    		j = 100;
    		cout<< "ADT construct i="<< i<< "j="<cout<< "ADT destruct"<< endl;
    	}
    };
    int main()
    {char *p = new(nothrow) char[sizeof ADT + 1];
    	if (p == NULL) {cout<< "alloc failed"<< endl;
    	}
    	ADT *q = new(p) ADT;  //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可
    	//delete q;//错误!不能在此处调用delete q;
    	q->ADT::~ADT();//显示调用析构函数
    	delete[] p;
    	return 0;
    }
    //输出结果:
    //ADT construct i=10j=100
    //ADT destruct
1.14 delete p、delete [] p、allocator都有什么作用?
  • delete p ,为消除一个对象。

  • delete[]时,数组中的元素按逆序的顺序进行销毁;

  • new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。

1.15 malloc与free的实现原理?

1、 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、munmap这些系统调用实现的;

2、 brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;

3、 malloc小于128k的内存,使用brk分配内存,将_edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。

4、 malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

1.16 malloc、realloc、calloc的区别
  • malloc函数

    void* malloc(unsigned int num_size);
    int *p = malloc(20*sizeof(int));申请20个int类型的空间;
  • calloc函数

    void* calloc(size_t n,size_t size);
    int *p = calloc(20, sizeof(int));

    省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;

  • realloc函数

    void realloc(void *p, size_t new_size);

    给动态分配的空间分配额外的空间,用于扩充容量。

1.17 exit()和return 的区别
  • return是语言级的,标志调用堆栈的返回。是从当前函数的返回,main()中return的退出程序
  • exit()是函数,强行退出程序,并返回值给系统
  • return实现函数逻辑,函数的输出。exit()只用来退出。
1.18 extern和export的作用

变量的声明有两种情况:

  1. 一种是需要建立存储空间的。例如:int a 在定义的时候就已经建立了存储空间。

  2. 另一种是不需要建立存储空间的。 例如:extern int a 其中变量a是在别的文件中定义的。

  3. 总之就是:把建立空间的声明成为“定义”,把不需要建立存储空间的成为“声明”。

  • extern
    • 普通变量、类。结构体
  • export(C++中新增)
    • 和exturn类似,但是用作模板
    • 使用该关键字可实现模板函数的外部调用
    • 模板实现的时候前面加上export,别的文件包含头文件就可用该模板
extern"C"的用法

在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。**所以使用extern "C"全部都放在于cpp程序相关文件或其头文件中。
C++中调用C代码:

//xx.h
extern int add(...)

//xx.c
int add(){}

//xx.cpp
extern "C" {#include "xx.h"
}

C调用C++函数

//xx.h
extern "C"{int add();
}
//xx.cpp
int add(){}
//xx.c
extern int add();
1.19 C++中,explicit的作用

explicit阻止隐式转换

  • 隐式转换

    String s1 = "hello";
    //进行隐式转换,等价于
    String s1 = String("hello");
  • explicit阻止隐式转换

    class Test1
    {public:
    	Test1(int n){num = n }
    private:
    	int num;
    }
    
    class Test2
    {public:
    	explicit Test2(int n){num = n }
    private:
    	int num;
    }
    
    int main()
    {Test1 t1 = 1; //正确,隐式转换
    	Test2 t2 = 1;//错误,禁止隐式转换
    	Test2 t2(1); //正确,可与显示调用
    }
1.20 C++的异常处理

C++中的异常处理机制主要使用try、throw和catch三个关键字

#includeusing namespace std;
int main()
{double m = 1, n = 0;
    try {cout<< "before dividing."<< endl;
        if (n == 0)
            throw - 1;  //抛出int型异常
        else if (m == 0)
            throw - 1.0;  //拋出 double 型异常
        else
            cout<< m / n<< endl;
        cout<< "after dividing."<< endl;
    }
    catch (double d) {cout<< "catch (double)"<< d<< endl;
    }
    catch (...) {cout<< "catch (...)"<< endl;
    }
    cout<< "finished"<< endl;
    return 0;
}
//运行结果
//before dividing.
//catch (...)
//finished

代码中,对两个数进行除法计算,其中除数为0。可以看到以上三个关键字,

  • 程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,
  • throw可以抛出各种数据类型的信息,代码中使用的是数字,也可以自定义异常class。catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(…)的方式捕获任何异常(不推荐)。
  • 当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面再throw异常。
1.21 回调函数
  • 把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调。

  • 如果代码立即被执行就称为同步回调,如果过后再执行,则称之为异步回调。

  • 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

  • 主函数和回调函数是在同一层的,而库函数在另外一层。如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数

  • sort(),中自定义的cmp就是回调函数

1.22 C++中,mutable的作用

mutable的中文意思是“可变的,易变的”,在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

class person
{int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
     void add() const//在函数里不可修改this指针指向的值 常量指针
     {m_A=10;//错误  不可修改值,this已经被修饰为常量指针
        m_B=20;//正确
     }
}

int main()
{const person p;//修饰常对象 不可修改类成员的值
	p.m_A=10;//错误,被修饰了指针常量
	p.m_B=200;//正确,特殊变量,修饰了mutable
}
2. 内存分配 2.1 C++内存分配

见 1.8

2.2 内存泄漏 内存泄露的原因

内存泄漏是指堆内存的泄漏。使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,如果没有释放内存这块内存就不能被再次使用,我们就说这块内存泄漏了

避免内存泄露的几种方式
  • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
  • 一定要将基类的析构函数声明为虚函数(这样子类的析构函数必须重新实现,避免忘记释放内存)
  • 对象数组的释放一定要用delete []
  • 有new就有delete,有malloc就有free,保证它们一定成对出现
内存泄漏检测工具
  • 从Linux下可以使用Valgrind工具
  • Windows下可以使用CRT库
2.3 栈默认的大小
  • Windows 下是2MB
  • Linux下是8MB(ulimit-s 设置)
2.4 sizeof() 和 strlen()的区别
int a ,b;
a = strlen("\0");
b = sizeof("\0");
// a = 0 , b = 2;
  • sizeof()是c关键字,计算内存大小,字节单位

  • strlen()是函数,计算字符串的长度,到\0结束

  • sizeof是编译确定的,strlen是运行确定的

    int a,b,c;
    char str[20] = "0123456789";
    const char *str2 = "0123456789";
    a = strlen(str);
    b = sizeof(str);
    c = sizeof(&str);
    d = strlen(str2);
    e = sizeof(str2);
    // a = 10 , b = 20 , c = 4(指针大小);
    // d = 10 , e = 4(指针大小)
2.5 struct结构体的数据对齐 为什么结构体的sizeof返回值一般大于期望?
  • struct的sizeof是所有成员数据对其后长度相加
  • union的sizeof是取大的成员长度(所有成员共用一个内存)
struct数据对其的目的?
  • 是编译器的一种计算手段,在空间和复杂度上的平衡,在空间浪费可接收的前提下cpu运算最快处理

  • 32位数据传输是4字节(数据字长),struct进行4的倍数对其。64位数据传输是8字节,8的倍数对其

  • 对齐的目的是要让数据访问更高效,一般来说,数据类型的对齐要求和它的长度是一致的,比如,

    char 是 1
      short 是 2
      int 是 4
      double 是 8
    • 这不是巧合,比如short,2对齐保证了short只可能出现在一个读取单元的0, 2, 4, 6格,而不会出现在1, 3, 5, 7格;
    • 再比如int,4对齐保证了一个读取单元可以装载2个int——在0或者4格。
    • 从根本上杜绝了同一个数据横跨读取单元的问题。
修改默认的数据对齐
  • #pragma pack(n),编译器按照n字节对其
  • #pragma pack( ),取消自定义对其
  • __ attribute__((aligned(n))) ,让结构体成员对其在n字节自然边界上,如果成员大于n,按照大成员长度
  • __ attribute__((packed)),取消编译过程的对齐,按照实际占用字节对其
C++11中内存对其关键字
  • alignof,计算出类型对齐的方式
  • alignas,指定结构体的对齐方式
struct Info {uint8_t a;
  uint16_t b;
  uint8_t c;
};

std::cout<< sizeof(Info)<< std::endl;   // 6  2 + 2 + 2
std::cout<< alignof(Info)<< std::endl;  // 2

//alignas将内存对齐调整为4个字节。所以sizeof(Info2)的值变为了8。
struct alignas(4) Info2 {uint8_t a;
  uint16_t b;
  uint8_t c;
};

std::cout<< sizeof(Info2)<< std::endl;   // 8  4 + 4
std::cout<< alignof(Info2)<< std::endl;  // 4

若alignas小于自然对齐的最小单位,则被忽略。

2.6 堆和栈的区别
  • 申请方式不同。

    • 栈由系统自动分配。
    • 堆是自己申请和释放的。
  • 申请大小限制不同。

    • 栈顶和栈底是之前预设好的,栈是向栈底扩展,大小固定,可以通过ulimit -a查看,由ulimit -s修改。

    • 堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。

  • 申请效率不同。

    • 栈由系统分配,速度快,不会有碎片。

    • 堆由程序员分配,速度慢,且会有碎片。

  • 栈空间默认是4M, 堆区一般是 1G - 4G

  • 速度不同

    • 毫无疑问是栈快一点。

    • 因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。

    • 而堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。

2.7 形参和实参的区别
  • 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。

  • 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。

  • 实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。

  • 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。

  • 当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

3. 指针 3.1 指针的优点?

指针变量和一般变量区别,一般变量是包含的是数据,而指针变量包含的是地址

  • 动态分配内存,直接操作内存,效率高
  • 实现动态数据结构(树、链表)
  • 高效的“复制”数据
3.2 引用和指针
  • 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
  • 指针可以有多级,引用只有一级
  • 指针可以为空,引用不能为NULL且在定义时必须初始化
  • 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
  • sizeof指针得到的是本指针的大小(4字节),sizeof引用得到的是引用所指向变量的大小
  • 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
  • 引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(具体情况还要具体分析)。
  • 引用在声明时必须初始化为另一变量;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
3.3 数组和指针
  • 数组在内存中是连续存放的,开辟一块连续的内存空间;数组所占存储空间:sizeof(数组名);数组大小:sizeof(数组名)/sizeof(数组元素数据类型);

  • 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof( p ),p 为指针得到的是一个指针变量的字节数(4),而不是p 所指的内存容量。

  • 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。

  • 在向函数传递参数的时候,如果实参是一个数组,那用于接受的形参为对应的指针。也就是传递过去是数组的首地址而不是整个数组,能够提高效率;

  • 在使用下标的时候,两者的用法相同,都是原地址加上下标值,不过数组的原地址就是数组首元素的地址是固定的,指针的原地址就不是固定的。

数组名和指针
  • 二者均可通过增减偏移量来访问数组中的元素。

  • 数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

  • 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

3.4 指针的加法

指针加上n,为加上n个指针类型的长度

unsigned char*p1 = 0x801000;
unsigned int *p2 = 0x810000;
p1+=5;//p1 = 0x801000 + 5*1 = 0x801005;
p2+=5;//p2 = 0x810000 + 5*4 = 0x810000;
3.5 空指针、野指针和悬空指针
  • 空指针
    空指针不会指向任何地方,它不是任何对象或函数的地址

    int *p = NULL;
    int *p2 = nullptr;
  • 野指针
    指的是没有被初始化过的指针

    int main(void) {
        int* p;     // 未初始化
        std::cout<< *p<< std::endl; // 未初始化就被使用
        
        return 0;
    }
  • 悬空指针
    最初指向的内存已经被释放了的一种指针

    int main(void) {  int * p = nullptr;
      int* p2 = new int;
      p = p2;
      delete p2;
    }

    此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料

    野指针和悬空指针的产生和解决
  • 野指针:指针变量未及时初始化 =>定义指针变量及时初始化,要么置空。

  • 悬空指针:指针free或delete之后没有及时置空 =>释放操作后立即置空。
    或使用智能指针(避免悬空指针产生)

3.6 指针函数和函数指针的区别 指针函数
  • 返回值为指针类型的函数

    #includeint* fun(int* x)    //传入指针  
    {int* tmp = x;	  //指针tmp指向x
        return tmp;       //返回tmp指向的地址
    }
    int main()
    {int b = 2;      
        int* p = &b;   //p指向b的地址
        printf("%d",*fun(p));//输出p指向的地址的值
        return 0;
    }
函数指针
  • 函数指针是 指向函数的指针 。主体是指针,指向的是一个函数的地址

  • 两种方法赋值:指针名 = 函数名; 指针名 = &函数名

    #includeint add(int x,int y)
    {return x + y;
    }
    
    int main()
    {int (*fun) (int,int);//声明函数指针
        fun = &add;		//fun函数指针指向add函数
        //fun = add;   //同上,等价fun = &add;
        printf("%d ",fun(3,5));		
        printf("%d",(*fun)(4,2));
        return 0;
    }
3.7 传递函数参数的时候,什么时候使用指针,什么时候使用引用
  • 需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的

  • 对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小

  • 类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式

3.8 区别指针类型
int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
  • int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。

  • int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。

  • int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。

  • int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。

3.9 int a[10]; int (*p)[10] = &a中a和&a有什么区别?
  • a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。
  • &a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。
  • 若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。
3.10 值传递、指针传递、引用传递的区别和效率
  • 值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象 或是大的结构体对象,将耗费一定的时间和空间。(传值)

  • 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)

  • 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)

效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

4. 预处理

为编译做准备工作,处理#开头的指令
在这里插入图片描述

4.1 ifndef/define/endif的作用

防止头文件被重复包含和编译。头文件重复包含会增大程序大小,重复编译增加编译时间

4.2 #include< >和 #include“ ”的区别
  • <>和" "表示编译器在搜索头文件时的顺序不同,
  • <>表示从系统目录下开始搜索,然后再搜索PATH环境变量所列出的目录,不搜索当前目录,
  • ""是表示从当前目录开始搜索,然后是系统目录和PATH环境变量所列出的目录。
    所以,系统头文件一般用<>,用户自己定义的则可以使用"",加快搜索速度。
4.3 #define 的缺点

#define只能进行字符替换

  • 无法类型检查

  • 由于优先级的不同,会产生潜在问题

    #define MAX_NUM 100+1
    int a = MAX_NUM * 10;//a=110
    //等价于
    int a = 100 + 1 * 10;
    
    //正确定义为
    #define MAX_NUM (100+1)
    int a = MAX_NUM * 10;//a=1010
  • 无法单步调试

  • 导致代码膨胀

4.4 写一个标准宏MIN
#define MIN(A,B) ( (A)<=(B)?(A):(B) )

每个括号都是必须的,如果没有结果无法预测

4.5 #define和typdef的区别
  • define主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。

  • define替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。

  • define不检查类型;typedef会检查数据类型。

  • define不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。

  • 对指针的操作不同

    #define INTPTR1 int*
    typedef int* INTPTR2;
    INTPTR1 p1, p2;//声明一个指针变量p1和一个整型变量p2
    INTPTR2 p3, p4;//声明两个指针变量p3、p4
    #define INTPTR1 int*
    typedef int* INTPTR2;
    int a = 1;
    int b = 2;
    int c = 3;
    const INTPTR1 p1 = &a;//const INTPTR1 p1是一个常量指针
    const INTPTR2 p2 = &b;//const INTPTR2 p2是一个指针常量
    INTPTR2 const p3 = &c;//INTPTR2 const p3是一个指针常量
4.6 宏定义和内联函数的区别
  • 宏定义是在预处理阶段进行代码替换,内联函数是编译阶段插入代码
  • 宏定义没有类型检查,内敛函数有类型检查
内联函数和普通函数的区别
  • 编译器将内联函数的位置进行函数展开,避免函数调用的开销,提高效率
  • 普通函数被调用,跳跃到函数入口地址,执行结束后跳转回调用地方
  • 内敛函数不需要寻址,执行N次内联函数,代码就复制N次
  • 函数体过大,编译器放弃内联,变的和普通函数一样
  • 内联函数不能递归,编译器无法预知深度,变成普通函数
4.7 #define和const区别?
  • #define只能单纯文本替换,不分配周期,寸在代码段
  • const常量在程序数据段,分配内存
  • #define没有数据类型,const有数据类型
  • #define没法调试,const可以调试
5.结构体与类 5.1 struct和union的区别
  • 联合体所有成员共用一块内存,结构体成员占用空间累加
  • 对联合体的不用成员赋值,对其他成员重写;结构体成员互相不影响
union
{int i;
	char x[2];
}
int main()
{a.x[0] = 10;
	a.x[1] = 1;
	printf("%d",a.i);//输出为266
}

其中 a.x[0]=10=00001010 ;a.x[1] = 1 = 00000001。
输出 i 的时候,将a.x[0] a.x[1] 看作一个整数,为00000001 00001010,为256+8+2 = 266

在这里插入图片描述

5.2 C++中struct和class的区别

相同点

  • 两者都拥有成员函数、公有和私有部分

  • 任何可以使用class完成的工作,同样可以使用struct完成
    不同点

  • 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的

  • class默认是private继承, 而struct默认是public继承

5.3 C++和C的struct区别
  • C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,(C++中的struct能继承,能实现多态)

  • C中struct是没有权限的设置的,且struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数

  • C++中,struct增加了访问权限,且可以和类一样有成员函数,成员默认访问说明符为public(为了与C兼容)

  • struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当作类的一种特例

6. 位操作 6.1 最有效的计算2乘8的方法
int a = 2 ;
a = a<<3;//a乘上2的三次方
计算乘7倍
int a = 2 ;
a = (a<<3)-a;
6.2 位操作求两个数的平均值
int a = 2 .b =3;
int c ;
c = (a&b) + ((a^b)>>1);
  • 对于表达式(x&y)+(xy)>>1), x&y表示的是取出x与y二进制位数中都为1的所有位, xy表示的是x与y中有一个为1’的所有位,右移1位相当于执行除以2运算。
  • 整个表达式实际上可以分为两部分,第一部分是都为1的部分,求平均数后这部分的值保持不变;而第二部分是x为1、y为0的部分,以及y为1、x为0的部分,两部分加起来再除以2,然后跟前面的相加就可以表示两者的平均数了
6.3 什么是大端和小端,如何判断?
  • 小端存储:字数据的低字节存储在低地址中(数据存储从低字节到高字节)
  • 大端存储:字数据的高字节存储在低地址中(数据存储从高字节到低字节)
    例如:32bit的数字0x12345678

在这里插入图片描述

代码判断
  • 方式一:使用强制类型转换-这种法子不错
#includeusing namespace std;
int main()
{int a = 0x1234;
    //由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
    char c = (char)(a);
    if (c == 0x12)
        cout<< "big endian"<< endl;
    else if(c == 0x34)
        cout<< "little endian"<< endl;
}
  • 方式二:巧用union联合体
#includeusing namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的大值
union endian
{int a;
    char ch;
};
int main()
{endian value;
    value.a = 0x1234;
    //a和ch共用4字节的内存空间
    if (value.ch == 0x12)
        cout<< "big endian"<
7. 编译 7.1 main函数执行前和执行后的代码 main函数执行前(初始化系统相关资源)
  • 设置栈指针
  • 初始化静态static变量和global全局变量,即.data段的内容
  • 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等等,即.bss段的内容
  • 全局对象初始化,在main之前调用全局对象的构造函数,这是可能会执行前的一些代码
  • 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数
  • __ attribute __((constructor))
main函数执行后
  • 全局对象的析构函数会在main函数之后执行;
  • 可以用 atexit 注册一个函数,它会在main 之后执行;
  • __ attribute__((destructor))
8.面向对象 8.1 final和override关键字 override

override指定了子类的这个虚函数是重写的父类的,如果你名字不小心打错了的话,编译器是不会编译通过的。

class A
{virtual void foo();
};
class B : public A
{virtual void f00(); //OK,这个函数是B新增的,不是继承的
    virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错
    //virtual void foo() override; //ok,是继承父类的虚函数
};
final

不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。

class Base
{virtual void foo();
};
 
class A : public Base
{void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};

class B final : A // 指明B是不可以被继承的
{void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{};
8.2 拷贝初始化和直接初始化

当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:

  • 直接初始化直接调用与实参匹配的构造函数
  • 拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
//语句1 直接初始化
string str1("I am a string");
//语句2 直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化
string str2(str1);
//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str3 = "I am a string";
//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
string str4 = str1;
  • 为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价),但是需要辨别两种情况。
    • 当拷贝构造函数为private时:语句3和语句4在编译时会报错
    • 使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错
8.3 C和C++的类型安全

类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。

  • 想保证程序的类型安全性,
    • 应尽量避免使用空类型指针void*,
    • 尽量不对两种类型指针做强制转换。
8.4 C++中的重载、重写(覆盖)和隐藏的区别 重载(overload)

重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。

class A{...
    virtual int fun();
    void fun(int);
    void fun(double, double);
    static int fun(char);
    ...
}
重写(覆盖)(override)

重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:

  • 与基类的虚函数有相同的参数个数
  • 与基类的虚函数有相同的参数类型
  • 与基类的虚函数有相同的返回值类型
//父类
class A{public:
    virtual int fun(int a){}
}
//子类
class B : public A{public:
    //重写,一般加override可以确保是重写父类的函数
    virtual int fun(int a) override{}
}

重载与重写的区别:

  • 重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
  • 重写要求参数列表相同,重载则要求参数列表不同,返回值不要求
隐藏(hide)

隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数,包括以下情况:

  • 两个函数参数相同,但是基类函数不是虚函数。和重写的区别在于基类函数是否是虚函数。

    //父类
    class A{public:
        void fun(int a){cout<< "A中的fun函数"<< endl;
    	}
    };
    //子类
    class B : public A{public:
        //隐藏父类的fun函数
        void fun(int a){cout<< "B中的fun函数"<< endl;
    	}
    };
    int main(){B b;
        b.fun(2); //调用的是B中的fun函数
        b.A::fun(2); //调用A中fun函数
        return 0;
    }
  • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。

    //父类
    class A{public:
        virtual void fun(int a){cout<< "A中的fun函数"<< endl;
    	}
    };
    //子类
    class B : public A{public:
        //隐藏父类的fun函数
       virtual void fun(char* a){   cout<< "A中的fun函数"<< endl;
       }
    };
    int main(){B b;
        b.fun(2); //报错,调用的是B中的fun函数,参数类型不对
        b.A::fun(2); //调用A中fun函数
        return 0;
    }
  • 基类指针指向派生类对象时,基类指针可以直接调用到派生类的覆盖(重写)函数,也可以通过 :: 调用到基类被覆盖
    的虚函数;

  • 而基类指针只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数。

    // 父类
    class A {public:
        virtual void fun(int a) {// 虚函数
            cout<< "This is A fun "<< a<< endl;
        }  
        void add(int a, int b) {cout<< "This is A add "<< a + b<< endl;
        }
    };
    
    // 子类
    class B: public A {public:
        void fun(int a) override {// 覆盖(重写)
            cout<< "this is B fun "<< a<< endl;
        }
        void add(int a) {// 隐藏
            cout<< "This is B add "<< a + a<< endl;
        }
    };
    
    int main() {A *p = new B();
        p->fun(1);      // 调用子类 fun 覆盖函数
        p->A::fun(1);   // 调用父类 fun
        p->add(1, 2);
        // p->add(1);      // 错误,识别的是 A 类中的 add 函数,参数不匹配
        // p->B::add(1);   // 错误,无法识别子类 add 函数
        return 0;
    }
8.5 浅拷贝和深拷贝的区别

浅拷贝

浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

深拷贝

深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。

8.6 public,protected和private访问和继承权限 访问权限

在这里插入图片描述

public、protected、private 的访问权限范围关系:public >protected >private

继承权限
  1. public继承
    公有继承的特点是,基类的公有和保护,变派生类的公有和保护,基类私有不可访问

  2. protected继承
    保护继承的特点是,基类的公有和保护,变派生类的保护,基类私有派生类不可访问

  3. private继承
    私有继承的特点是,基类的公有和保护,变派生类的私有,基类私有派生类不可访问

在这里插入图片描述

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


分享名称:C/C++重点八股文-创新互联
URL分享:http://ybzwz.com/article/dgjpos.html