C++11新特性介绍,源码测试

12

这里写目录标题

  • 关键字
    • auto
    • decltype
    • nullptr
    • final
    • override
    • default
    • delete
  • 右值引用和std::move
  • Lambda表达式
  • for循环(基于范围的循环)
  • 统一初始化initializer_list
  • 静态断言static_assert
  • 函数返回类型后置
  • 强类型枚举(枚举类)
  • 前置枚举声明
  • 内联命名空间(Inline namespaces)
  • 变参宏(Variadic macros)

关键字

auto

自动类型推导,auto声明变量的类型必须由编译器在编译时期推导而得。

    //1.复杂类型变量声明时简化代码
    std::vector<int> v_int;
    v_int.push_back(101);
//    std::vector::iterator iter = v_int.begin();//迭代器类型复杂
    auto iter = v_int.begin();//使用auto关键字自动推导类型,简介易读
    while(iter != v_int.end())
    {
        printf("value=%d\n",*iter);
        iter++;
    }

    //2.自适应泛型编程,根据不同的编译环境,或者引用动态库版本升级,导致表达式返回值类型变化,auto可以自适应类型,无需改动代码
    auto v_len = strlen("hello world!");//32位返回4字节整型,64位返回8字节整型
    printf("v_len=%ld,sizeof(v_len)=%ld\n",v_len,sizeof(v_len));

    //3.宏定义的性能提升
    //MAX1是传统写法,a和b是表达式时,无论返回a还是b,a或b都会被运算两次
    //MAX2中先把a和b计算出来,再进行比较,a和b都只计算了一次
#define MAX1(a, b) ((a) > (b)) ? (a) : (b)
#define MAX2(a, b) ({\
        auto _a = (a);\
        auto _b = (b);\
        (_a > _b) ? _a : _b;})
    int num1 = MAX1(1*2*3*4, 5+6+7+8);
    int num2 = MAX2(1*2*3*4, 5+6+7+8);
    printf("num1=%d\n",num1);
    printf("num2=%d\n",num2);

打印

value=101
v_len=12,sizeof(v_len)=8
num1=26
num2=26

decltype

和auto的功能类似,decltype用来在编译时期进行自动类型推导,引入decltype是因为auto并不适用于所有的自动类型推导场景。

1、 auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系。
2、auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导,而decltype不要求。

auto varName=value;
decltype(exp) varName=value;
decltype(exp) varName;//可以不初始化

decltype的推导规则可以简单概述如下:

1、如果exp是一个不被括号()包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,decltype(exp)的类型和exp一致。
2、如果exp是函数调用,则decltype(exp)的类型就和函数返回值的类型一致。
3、如果exp是一个左值,或被括号()包围,decltype(exp)的类型就是exp的引用,假设exp的类型为T,则decltype(exp)的类型为T&。

int num2 = 2;
int& func1(int num,char c)//返回值为int&
{
    printf("num=%d\n",num);

    return num2;
}

    int n=0;
    const int &r=n;
    decltype(n) x=n;    //n为Int,x被推导为Int
    decltype(r) y=n;    //r为const int &,y被推导为const int &

    decltype(func1(100,'A')) a=n;//a的类型为int&,func1函数不会真正执行,只是形式

    int n2=0,m2=0;
    decltype(m2+n2) c=0;//n+m得到一个右值,c的类型为int
    decltype(n2=n2+m2) d=c;//n=n+m得到一个左值,d的类型为int &

    c = 102;
    printf("c=%d,d=%d\n",c,d);//d是c的引用
    //左值:表达式执行结束后依然存在的数据,即持久性数据;
    //右值是指那些在表达式执行结束不再存在的数据,即临时性数据。

打印

c=102,d=102

nullptr

C++11使用nullptr取代NULL表示空指针.
NULL缺陷:在VS等编译环境下NULL是宏定义,值为0,也就是0x0000 0000这个内存空间,NULL即表示空指针,又表示0,在某些代码场景存在二意性。

void Func_nul(void* ){
       printf("I am void fucntion!\n");
}

void Func_nul(int ){
       printf("I am zero fucntion!\n");
}

    Func_nul(NULL);//编译错误:error: call to 'Func_nul' is ambiguous
    Func_nul(0);

nullptr是nullptr_t类型的右值常量,专门用于初始化空类型的指针。nullptr_t是在C++11新增加的数据类型,nullptr_t是指针空值类型。

nullptr可以被隐式转换成任意的指针类型, 不同类型的指针变量都可以使用nullptr类初始化,编译器会将nullptr隐式转换成int*、char*、double*指针类型。

final

final 关键字限制某个类不能被继承,或者某个虚函数不能被重写。如果使用 final 修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面。

final修饰类
被final修饰的类不能作为基类,也就是说不能被其他类所继承。
举例:类A使用final修饰,类B继承于A时报错:error: base ‘A’ is marked ‘final’。

class A final
{
public:
    virtual void TestFunc()
    {
        printf("A Class\n");
    }
};

class B :A//error: base 'A' is marked 'final'
{
public:
    virtual  void TestFunc()
    {
        printf("B Class\n");
    }
};

final修饰函数
被修饰的虚函数禁止了子类对虚函数的重写。

class A
{
public:
    virtual void TestFunc() final
    {
        printf("A Class\n");
    }
};

class B:A
{
public:
    virtual  void TestFunc()//error: declaration of 'TestFunc' overrides a 'final' function
    {
        printf("B Class\n");
    }
};

override

在成员函数声明或定义中, override 确保该函数为虚函数并覆盖来自基类的虚函数。
override 显示地表明,这个函数是重写基类的虚函数,编译器可以帮助验证 override 对应的方法名是否是基类中所有的,如果没有则报错。
下面例子,类B虚函数TestFunc使用override 修饰,但基类A中并没有这个虚函数,因此报错。

class A
{
public:
};

class B :public A
{
public:
    //error: 'TestFunc' marked 'override' but does not override any member functions
    //如果不使用override ,则不会编译报错
    virtual  void TestFunc() override = 0;
};

override带来的好处
1、如果基类中没有该虚函数,则编译会报错。
2、防止在重写基类虚函数时,函数名写错,如果没有override 修饰,函数名写错了编译并不会报错。

default

C++类有4个特殊成员函数:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符。如果程序没有定义这些特殊成员函数,则编译器会隐式的为这个类生成一个默认的特殊成员函数。

这几种特殊成员函数使用default关键字,可以快速生成一个默认成员函数,而不需要写函数体。

default的好处
1、写法简单,节省开发时间。
2、代码执行效率高,当我们使用这个关键字定义的构造函数,在声明变量时,编译器不会去调用构造函数,也不会生成构造函数的代码,高效率提高声明变量的时间。

class School{
public:
    School() = default;
    virtual ~School() = default;
private:
    std::string name;
};

class college : public School{
public:
    int age;
    ~college()
    {
        printf("~college\n");
    }
};

    School *pSchool = new college;
    delete pSchool;

上面例子,如果School的析构函数不是virtual的,则delete时不会执行college类的析构函数,造成隐式的内存泄漏。

delete

C++11可以使用delete关键字显示地删除默认生成的函数,比如删除一个函数模板的实例,删除类的默认拷贝构造函数等。

template <class T>
T sum(T t1, T t2)
{
    return t1 + t2;
}
int sum(int, int) = delete ;//删除函数模板的一个实例

class Student
{
public:
    Student() = default;
    Student(const Student & c) = delete ;//删除拷贝构造函数

    int age;
};

//    sum(1,2);//error: call to deleted function 'sum'
    float num = sum(1.2, 3.5);
    printf("num=%.2f\n",num);//num=4.70

    Student s1;
//    Student s2 = s1;//error: call to deleted constructor of 'Student'

右值引用和std::move

传统的C++语法中就有引用,C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

1、左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
2、右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
3、一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
4、左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象 。
5、右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。

左值引用和右值引用

左值引用:type &引用名 = 左值表达式;对左值的引用,是给左值取别名。
右值引用:type &&引用名 = 右值表达式;对右值的引用,是给右值取别名。

    int a1=10;//a1 是左值
    int & a2=a1;//引用左值,是一个左值引用

    int&& b2 = 100;//右值引用
    printf("a2=%d,b2=%d\n",a2,b2);//a2=10,b2=100

std::move
将一个左值引用强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。

std::move语句可以将左值变为右值而避免拷贝构造。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。

    int a = 1;
    std::vector<int> vec;
    vec.push_back(std::move(a));
    printf("a=%d\n",a);//基本数据类型不存在拷贝构造,a的值不变

    std::string str = "Hello";
    std::vector<std::string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据,str的值不变
    v.push_back(str);
    printf("str=%s\n",str.c_str());

    //调用移动构造函数,会把原str的数据据为己有,最好不要使用str
    v.push_back(std::move(str));
    printf("str=%s\n",str.c_str());

    auto iter = v.begin();
    while(iter != v.end())
    {
        printf("v.str=%s\n",iter->c_str());
        iter++;
    }

打印

a=1
str=Hello
str=
v.str=Hello
v.str=Hello

Lambda表达式

Lambda(匿名函数)表达式是C++11最重要的特性之一,来源于函数式编程的概念。

声明式编程风格:就地匿名定义目标函数或函数对象,有更好的可读性和可维护性。
简洁:不需要额外写一个命名函数或函数对象,,避免了代码膨胀和功能分散。
更加灵活:在需要的时间和地点实现功能闭包。

lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。语法形式如下:

[ capture ] ( params ) opt -> ret { body; };

auto f = [](int a) -> int {return a + 1;};
auto f = [](int a) {return a + 1;};//省略返回值的定义

capture:捕获列表
params:参数列表
opt:函数选项
ret:返回值类型
body:函数体

在实际的使用中,可以省略其返回值的定义(opt -> ret),使用auto自动推导返回值类型。

捕获变量规则

[] 不捕获任何变量
[&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)
[=]捕获外部作用域中所有变量,并作为副本在函数体重使用(按值捕获)
[=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获foo变量
[bar] 按值捕获bar变量,同时不捕获其他变量
[this] 捕获当前类中的this指针,让表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lambda中使用当前类的成员变量和成员函数。

mutable
若( )后面使用mutable修饰,在=传值时,可以在表达式内修改参数,但只是修改了行参,并未修改参数本身的值。

传参示例

class A 
{
public:
	int i_ = 0;

	void func(int x,int y)
	{
		auto x1 = []{return i_;};  // error,没有捕获外部变量
		auto x2 = [=]{return i_ + x + y;}; //ok,按值捕获所有外部变量
		auto x3 = [&]{return i_ + x + y;}; //ok,按引用捕获所有外部变量
		auto x4 = [this]{return i_;}; //ok,捕获this指针
		auto x5 = [this]{return i_ + x + y;}; //error,没有捕获x和y变量
		auto x6 = [this,x,y]{return i_ + x + y;}; //ok,捕获了this指针和x、y变量
		auto x7 = [this]{return i_++;}; //ok,捕获了this指针,修改成员变量的值
	}
};

int a = 0 , b = 0 ;
auto f5 = [a]{return a+b;}; //error,没有捕获b变量
auto f6 = [a,&b]{return a+ (b++);}; //ok,捕获a以及b的引用,对b进行自加
auto f7 = [=,&b]{return a+ (b++);}; //ok, 捕获所有外部变量和b的引用,对b进行自加

实际应用

    int a = 1;
    int b = 2;
    auto fun1 = [=](int c)mutable{
        b = a + c;
        return b;};
    printf("fun1(10)=%d\n",fun1(10));
    printf("b=%d\n",b);//值传递,并未修改b的值

    std::vector<int> v = {1,2,3,4,5,6,7};
    int sum = 0;
    for_each(v.begin(),v.end(),[&sum](int val){
            sum += val;
    });
    printf("sum=%d\n",sum);

打印

fun1(10)=11
b=2
sum=28

for循环(基于范围的循环)

C++ 11 标准中,为 for 循环添加了一种全新的语法格式,如下所示:

for (declaration : expression){
    //循环体
}

declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型,通常使用auto自动推导变量的类型。
expression:表示要遍历的序列,常见的可以为事先定义好的普通数组或者容器,还可以是用 {} 大括号初始化的序列。

    //1.for循环遍历普通数组,尾部\0也会被遍历
    char arc[] = "HelloWorld";
    for (char ch : arc)
    {
        if(ch == '\0')
            printf(" ");
        else
            printf("%c",ch);
    }
    printf(".\n");

    //2.for循环遍历 vector 容器
    std::vector<char>v1(arc, arc + 5);
    for (auto ch : v1)
    {
        printf("%c",ch);
    }
    printf(".\n");

    //3.遍历用{ }大括号初始化的列表
    for (int num : {1, 2, 3, 4, 5})
    {
        printf("%d",num);
    }
    printf(".\n");

    //4.引用遍历,可修改容器中的值
    char arc2[] = "abcde";
    std::vector<char>v2(arc2, arc2 + 5);
    for (auto &ch : v2)
    {
        ch++;
    }
    //for循环遍历输出容器中各个字符
    for (auto ch : v2)
    {
        if(ch == '\0')
            printf(" ");
        else
            printf("%c",ch);
    }
    printf(".\n");

打印

HelloWorld .
Hello.
12345.
bcdef.

统一初始化initializer_list

统一初始化,也叫做大括号初始化。就是使用大括号进行初始化的方式。
编译器看到{t1, t2, …, tn}便会做出一个initializer_list,它关联到一个array。调用构造函数的时候,该array内的元素会被编译器分解逐一传给函数。但若函数的参数就是initializer_list,则不会逐一分解,而是直接调用该参数的函数。

    //初始化示例
    int values[]{ 1, 2, 3 };
    std::vector<int> v{ 2, 3, 6, 7 };
    std::vector<std::string> cities{
        "Berlin", "New York", "London",  "Braunschweig"
    };
    
    //显示使用std::initializer_list
    class P
    {
    public:
        P(int, int){printf("call P::P(int,int)\n");}
        P(std::initializer_list<int>){
            printf("call P::P(initializer_list)\n");
        }
    };
    P p(77,5);     // call P::P(int,int)
    P q{77,5};     // call P::P(initializer_list)
    P r{77,5,42};  // call P::P(initializer_list)
    P s = {77, 5}; // call P::P(initializer_list)

打印

call P::P(int,int)
call P::P(initializer_list)
call P::P(initializer_list)
call P::P(initializer_list)

静态断言static_assert

与assert不同的是,static_assert是在编译期进行检查,而不是在运行期进行检查。static_assert的原理是在编译期检查一个条件是否满足,如果不满足则编译器会报错并输出错误信息。

static_assert通常用于编译期检查一些常量表达式或类型特性,可以帮助程序员在编译期发现一些错误,提高程序的健壮性和可维护性。

static_assert(expression, message);

其中,expression是一个常量表达式,用于检查某个条件是否满足;message是一个字符串,用于描述错误信息。

template <int N>
struct check_num
{
    static_assert(N > 0, "N must be greater than 0"); // 检查N是否大于0
    static const bool value = (N % 8) == 0;           // 检查N是否是8的整数倍
};

    int arr[8];//比如是7,则编译不通过
    // 检测arr占用字节数是否是8的整数倍
    static_assert(check_num<sizeof(arr)>::value, "The array size must be an integer multiple of 8");
    printf("Array size = %ld\n",sizeof(arr));

函数返回类型后置

把函数返回值写的(参数)的后面,下面例子,auto是占位符,->后面才是返回值类型。

auto Fun(int a, int b) ->int//等同于下面的写法
//int Fun(int a, int b)
{
    return a + b;
}

int sum = Fun(1,2);
printf("sum=%d\n",sum);//sum=3

用法一:返回一个函数指针类型
1、使用传统函数声明语法无法将函数指针类型作为返回类型直接使用,所以需要使用typedef给函数指针类型创建别名 bar,再使用别名作为函数的返回类型。
2、使用函数返回类型后置语法则没有这个问题。

int bar_impl(int x)
{
    return x*2;
}

typedef int(*bar)(int);
//传统前置返回值,需要用typedef定义别名
bar foo1()
{
    return bar_impl;
}

//使用后置返回值则不需要定义别名,直接使用即可
auto foo2()->int(*)(int)
{
    return bar_impl;
}

auto func = foo2();
printf("func(16)=%d\n",func(16));//func(16)=32

用法二:推导函数模板返回类型
配合decltype说明符,自动推导函数模板返回值类型。

template<class T1, class T2>
auto sum(T1 t1, T2 t2)->decltype(t1 + t2)
{
    return t1 + t2;
}

auto num = sum(4, 2);
printf("num=%d\n",num);//num=6

强类型枚举(枚举类)

枚举起源于C,所以C++的枚举直接从C语言继承过来。
C枚举不足
1、由于C语言是没有命名空间的,所以枚举的成员在C++中的作用域也是全局的。
2、由于枚举本质是常量数值,所以枚举成员会被隐式转换为整型。这提供了一些便捷性同样也带来了一定的风险。
3、枚举为int类型4字节,当超出时会溢出。(和操作系统相关,部分操作系统当超出4字节int范围时,枚举会自动扩容为8字节)。

C++枚举类
为了解决上述问题,C++11引入了枚举类(enum class)也叫强类型枚举(strong-typed enum)。
而枚举类的声明:在enum 的后面添加class 关键字。
优点:

强作用域:枚举类的成员会严格按照作用域空间。
隐式转换限制:枚举类的成员不可以和整型进行转换(可以强制类型转换)。
指定底层类型:枚举类默认的底层类型是int,还支持显式的指定底层类型,语法: enum_name : type。需要注意的是type是处理wchar_t(宽字符)之外的所有整型类型。

//原始枚举,从C语言继承,枚举值是全局变量
enum Enum
{
    A = 10,        //10
    B,             //11
    C = 10,        //10
    D,             //11
    //如果枚举整型大于int,枚举类型自动由int4字节升级为long int8字节,ubuntu20.04环境
};

enum class Enum1: long long int//指定枚举数据类型
{
    //强作用域,如果Enum1是普通枚举,则下面的枚举和Enum是重复定义
    A = 4,          //4
    B = 0,          //0
    C ,             //1
    D ,             //2
};

enum class Enum2: long long int
{
    //和Enum1不会重复定义
    A = 5,          //5
    B = 11,         //11
    C = LONG_MAX,   //9223372036854775807
    D = LLONG_MAX,  //9223372036854775807
};

    printf("sizeof(int)=%ld\n",sizeof(int));
    printf("sizeof(long int)=%ld\n",sizeof(long int));
    printf("sizeof(long long int)=%ld\n",sizeof(long long int));
    printf("sizeof(Enum::A)=%ld\n" ,sizeof(A));//4字节,如果定义了E = LONG_MAX,则是8字节
    printf("sizeof(Enum1::A)=%ld\n" ,sizeof(Enum1::A));

    printf("Enum::A=%d\n",A);//no warning
    printf("Enum2::A=%lld\n",Enum2::A);//warning: format ‘%lld’ expects argument of type ‘long long int’, but argument 2 has type ‘Enum2’ [-Wformat=]

    //原枚举,支持和整型的隐式转换
    int num = A;

    //枚举类,不支持和整型的隐式转换
//    long long int num1 = Enum1::A;//error: cannot initialize a variable of type 'long long' with an rvalue of type 'Enum1'
    long long int num1 = (long long int)Enum1::A;//强制类型转换,ok

    printf("num=%d\n",num);
    printf("num1=%lld\n",num1);

打印

sizeof(int)=4
sizeof(long int)=8
sizeof(long long int)=8
sizeof(Enum::A)=4
sizeof(Enum1::A)=8
Enum::A=10
Enum2::A=5
num=10
num1=4

前置枚举声明

C++开发中通常会把类、结构体、枚举定义在.h头文件中,有时会出现A.h包含了B.h,B.h也包含了A.h,交叉包含的问题。

使用前置声明的好处
1、防止头文件相互交叉包含。
2、C++中头文件不会单独编译,在cpp文件编译时会同时编译依赖的头文件,不使用前置声明会添加多余的头文件依赖,产生额外的编译开销。
3、一句话:尽量不要把包含头文件写在.h头文件,而是写在cpp文件。

在mainwindow.h中定义了结构体、类、枚举类,C++11支持枚举类前置声明。

struct Country{
    int  area;
    std::string name;
};

class Student
{
public:
    int age;
    std::string name;
};

enum class Enum1: long long int//指定枚举数据类型
{
    A = 4,          //4
    B = 0,          //0
    C ,             //1
    D ,             //2
};

TestEnum.h

#ifndef TESTENUM_H
#define TESTENUM_H

//前置声明
class Student;
struct Country;
enum class Enum1: long long int;
class TestEnum
{
public:
    TestEnum();
private:
    Enum1 m_Enum1;
    Student *pStu;
    Country *pCountry;
};

#endif // TESTENUM_H

TestEnum.cpp

#include "TestEnum.h"

#include "mainwindow.h"

TestEnum::TestEnum()
{
    m_Enum1 = Enum1::A;

    pStu = new Student;
    pStu->age = 10;

    pCountry = new Country;
    pCountry->area = 1000;

    printf("m_Enum1=%lld\n",m_Enum1);
    printf("pStu->age=%d\n",pStu->age);
    printf("pCountry->area=%d\n",pCountry->area);
}

在头文件使用前置声明,在cpp文件保护依赖头文件。
打印

m_Enum1=4
pStu->age=10
pCountry->area=1000

内联命名空间(Inline namespaces)

在namespace加inline关键字即可。
内联命名空间的特点时,不需要使用using语句,也不需使用命名空间前缀,就可以直接在外层命名空间使用该命名空间内部的内容。

当然既不使用using,也不使用命名空间前缀,则不同的内联命名空间不能定义相同的内容(比如同名类)。

内联命名空间带来的好处
1、省事,省的写using和命名空间前缀就可以直接使用空间里的内容。
2、在库开发中,库的版本迭代升级提供新的接口,同时保留旧的接口,就可以使用内联命名空间,为库的调用者提供便利。

//老接口
namespace inline_ns1{
    class AA{
    public:
        void testFunc()
        {
            printf("old class AA\n");
        }
    };
}

//新接口
inline namespace inline_ns2{
    class AA{
    public:
        void testFunc()
        {
            printf("class AA\n");
        }
    };
}

    //新接口,不使用命名空间前缀
    AA aa;
    aa.testFunc();

    //如果想使用老接口,加上命名空间前缀即可
    inline_ns1::AA aa2;
    aa2.testFunc();

打印

class AA
old class AA

变参宏(Variadic macros)

#include 
#include 

#define ONE_PARM(parm1)  printf("first=%-2c\n",(parm1))
#define TWO_PARM(parm1, parm2)  printf("first=%-2c,second=%-5d\n",(parm1),(parm2))
#define THREE_PARM(parm1, parm2, parm3) printf("first=%-2c,second=%-5d,third=%-20s\n",(parm1),(parm2),(parm3))

#define GET_PARM(_1,_2,_3,func,...) func

#define PRINTF(...) GET_PARM(__VA_ARGS__, THREE_PARM, TWO_PARM, ONE_PARM,...)(__VA_ARGS__)

    int num=1;
    char ch='A';
    std::string str = "string";

    PRINTF(ch);
    PRINTF(ch,num);
    PRINTF(ch,num,str.c_str());

打印

first=A 
first=A ,second=1    
first=A ,second=1    ,third=string   

你可能感兴趣的:(C/C++,c++,开发语言,c语言,linux,服务器)