C++11中静态局部变量初始化的线程安全性

前言

大家都知道,在C++11标准中,要求局部静态变量初始化具有线程安全性,所以我们可以很容易实现一个线程安全的单例类:

class Foo
{
public:
    static Foo *getInstance()
    {
        static Foo s_instance;
        return &s_instance;
    }
private:
    Foo() {}
};

在C++标准中,是这样描述的(在标准草案的6.7节中):

such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization. If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.

分析

标准关于局部静态变量初始化,有这么几点要求:

  1. 变量在代码第一次执行到变量声明的地方时初始化。
  2. 初始化过程中发生异常的话视为未完成初始化,未完成初始化的话,需要下次有代码执行到相同位置时再次初始化。
  3. 在当前线程执行到需要初始化变量的地方时,如果有其他线程正在初始化该变量,则阻塞当前线程,直到初始化完成为止。
  4. 如果初始化过程中发生了对初始化的递归调用,则视为未定义行为。

关于第4点,如果不明白,可以参考以下代码:

class Bar
{
public:
    static Bar *getInstance()
    {
        static Bar s_instance;
        return &s_instance;
    }
private:
    Bar()
    {
        getInstance();
    }
};

GCC的实现

以GCC 7.3.0版本为例,我们来分析GCC是如何实现标准的。

Foo::getInstance()

使用GCC编译后,我们使用gdb将文章开头的Foo::getInstance()反汇编:

Dump of assembler code for function Foo::getInstance():
   0x00005555555546ea <+0>:     push   %rbp
   0x00005555555546eb <+1>:     mov    %rsp,%rbp
=> 0x00005555555546ee <+4>:     movzbl 0x20092b(%rip),%eax        # 0x555555755020 <_ZGVZN3Foo11getInstanceEvE10s_instance>
   0x00005555555546f5 <+11>:    test   %al,%al
   0x00005555555546f7 <+13>:    sete   %al
   0x00005555555546fa <+16>:    test   %al,%al
   0x00005555555546fc <+18>:    je     0x55555555472b <Foo::getInstance()+65>
   0x00005555555546fe <+20>:    lea    0x20091b(%rip),%rdi        # 0x555555755020 <_ZGVZN3Foo11getInstanceEvE10s_instance>
   0x0000555555554705 <+27>:    callq  0x5555555545b0 <__cxa_guard_acquire@plt>
   0x000055555555470a <+32>:    test   %eax,%eax
   0x000055555555470c <+34>:    setne  %al
   0x000055555555470f <+37>:    test   %al,%al
   0x0000555555554711 <+39>:    je     0x55555555472b <Foo::getInstance()+65>
   0x0000555555554713 <+41>:    lea    0x2008fe(%rip),%rdi        # 0x555555755018 <_ZZN3Foo11getInstanceEvE10s_instance>
   0x000055555555471a <+48>:    callq  0x555555554734 <Foo::Foo()>
   0x000055555555471f <+53>:    lea    0x2008fa(%rip),%rdi        # 0x555555755020 <_ZGVZN3Foo11getInstanceEvE10s_instance>
   0x0000555555554726 <+60>:    callq  0x5555555545a0 <__cxa_guard_release@plt>
   0x000055555555472b <+65>:    lea    0x2008e6(%rip),%rax        # 0x555555755018 <_ZZN3Foo11getInstanceEvE10s_instance>
   0x0000555555554732 <+72>:    pop    %rbp
   0x0000555555554733 <+73>:    retq   
End of assembler dump.

+4+20+53出现的_ZGVZN3Foo11getInstanceEvE10s_instance使用c++filt分析为guard variable for Foo::getInstance()::s_instance,而+41+65位置出现的_ZZN3Foo11getInstanceEvE10s_instance则为Foo::getInstance()::s_instance。后者是s_instance这个局部静态变量,前者从名字看就知道是个guard标志变量,用来指示局部静态变量的初始化状态。

+4 ~ +18

测试guard变量的第一个字节,如果为0,代表s_instance未初始化,进入+27;否则代表s_instance已初始化,进入+65

+20 ~ +27

guard变量地址作为参数,执行__cxa_guard_acquire函数。

+32 ~ +39

测试返回值,如果为0,代表s_instance已初始化,进入+65;否则代表s_instance未初始化,进入+41

+41 ~ +48

初始化s_instance

+53 ~ +60

guard变量地址作为参数,执行__cxa_guard_release函数。

+65 ~ +73

返回s_instance地址

__cxa_guard_acquire

我们来看看__cxa_guard_acquire这个函数具体做了什么,该函数代码位于gcc-7-7.3.0/gcc-7.3.0/libstdc++-v3/libsupc++/guard.cc。由于这个函数针对不同平台做了不同的实现,有些我们不需要的代码,以我机器的设置,支持线程和futex系统调用,所以删除了一些不相关的代码:

int __cxa_guard_acquire (__guard *g)
{
    // If the target can reorder loads, we need to insert a read memory
    // barrier so that accesses to the guarded variable happen after the
    // guard test.

    // 1
    if (_GLIBCXX_GUARD_TEST_AND_ACQUIRE (g))
        return 0;

    // If __atomic_* and futex syscall are supported, don't use any global
    // mutex.

    // 2
    if (__gthread_active_p ())
    {
        int *gi = (int *) (void *) g;

        // 3
        const int guard_bit = _GLIBCXX_GUARD_BIT;
        const int pending_bit = _GLIBCXX_GUARD_PENDING_BIT;
        const int waiting_bit = _GLIBCXX_GUARD_WAITING_BIT;

        while (1)
        {
            // 4
            int expected(0);
            if (__atomic_compare_exchange_n(gi, &expected, pending_bit, false,
                                            __ATOMIC_ACQ_REL,
                                            __ATOMIC_ACQUIRE))
            {
                // This thread should do the initialization.
                return 1;
            }

            // 5
            if (expected == guard_bit)
            {
                // Already initialized.
                return 0;
            }

            // 6
            if (expected == pending_bit)
            {
                // Use acquire here.

                // 7
                int newv = expected | waiting_bit;

                // 8
                if (!__atomic_compare_exchange_n(gi, &expected, newv, false,
                                                 __ATOMIC_ACQ_REL,
                                                 __ATOMIC_ACQUIRE))
                {
                    // 9
                    if (expected == guard_bit)
                    {
                        // Make a thread that failed to set the
                        // waiting bit exit the function earlier,
                        // if it detects that another thread has
                        // successfully finished initialising.
                        return 0;
                    }

                    // 10
                    if (expected == 0)
                        continue;
                }

                // 11
                expected = newv;
            }

            // 12
            syscall (SYS_futex, gi, _GLIBCXX_FUTEX_WAIT, expected, 0);
        }
    }

    return acquire (g);
}
  1. 首先检测guard变量,guard变量等于1的话,直接返回0,代表s_instance已初始化,不需要再次初始化。
  2. 检测是否为多线程环境,如果没有多线程的话,也就没有必要去做额外工作来保证线程安全了。
  3. guard_bit表示s_instance已经初始化成功;pending_bit表示s_instance正在初始化;waiting_bit表示有其他线程正在等待s_instance的初始化。
  4. 使用一个原子操作来检测guard变量是否为0,如果为0,则由当前线程初始化s_instance,把pending_bit写入guard变量,返回1。如果不为0,则将guard当前值写入expected
  5. 检测expected值是否为guard_bit,如果是,则s_instance已初始化完成,不再需要初始化,返回0
  6. 检测expected值是否为pending_bit,如果是,说明s_instance正在初始化,且没有其他线程等待初始化。
  7. newv变量设置为pending_bit | waiting_bit,表示s_instance正在初始化且有线程正在等待初始化。
  8. 使用一个原子操作来检测guard变量是否为pending_bit,如果不是,说明有其他线程修改了guard变量,需要做进一步检测;如果是,说明没有其他线程修改guard变量,则将pending_bit | waiting_bit写入guard变量。
  9. 如果expected等于guard_bit,说明s_instance被初始化成功,不需要再初始化,返回0
  10. 如果expected等于0,说明s_instance初始化失败,回到4重新开始检测。
  11. 如果在8中没有其他线程修改过guard变量,将expected设置为pending_bit | waiting_bit,表示s_instance正在初始化且有线程(也就是当前线程)正在等待初始化。
  12. 如果在6处没有进入if分支,说明expected等于pending_bit | waiting_bit,如果进入了if分支,由11可得,此时expected也被修改为了pending_bit | waiting_bit。总之,此时s_instance正在初始化且有线程正在等待初始化。利用futex系统调用,再次检测guard变量是否发生了变化,如果发生了变化,回到4重新开始检测;如果没有发生变化,仍然等于pending_bit | waiting_bit,则挂起当前线程。

总之,__cxa_guard_acquire要么返回0要么返回1,用来指示s_instance已初始化或未初始化。__cxa_guard_acquire可能会导致当前线程挂起,这发生在s_instance正在初始化的时候。

__cxa_guard_release

由于__cxa_guard_acquire可能导致当前线程挂起,因此需要在s_instance初始化完成后使用将__cxa_guard_release线程唤醒。

void __cxa_guard_release (__guard *g) throw ()
{
    // If __atomic_* and futex syscall are supported, don't use any global
    // mutex.

    // 1
    if (__gthread_active_p ())
    {
        int *gi = (int *) (void *) g;
        const int guard_bit = _GLIBCXX_GUARD_BIT;
        const int waiting_bit = _GLIBCXX_GUARD_WAITING_BIT;

        // 2
        int old = __atomic_exchange_n (gi, guard_bit, __ATOMIC_ACQ_REL);

        // 3
        if ((old & waiting_bit) != 0)
            syscall (SYS_futex, gi, _GLIBCXX_FUTEX_WAKE, INT_MAX);
        return;
    }

    set_init_in_progress_flag(g, 0);
    _GLIBCXX_GUARD_SET_AND_RELEASE (g);
}
  1. 检测是否为多线程环境
  2. 使用原子操作将guard变量置为guard_bit,同时获取guard变量原始值。
  3. 如果guard变量原始值包含waiting_bit,说明有线程挂起(或将要调用futex欲使线程挂起),调用futex唤醒挂起的进程。

__cxa_guard_abort

由于s_instance可能初始化失败(本例中并未体现),因此还有一个__cxa_guard_abort函数。

void __cxa_guard_abort (__guard *g) throw ()
{
    // If __atomic_* and futex syscall are supported, don't use any global
    // mutex.
    if (__gthread_active_p ())
    {
        int *gi = (int *) (void *) g;
        const int waiting_bit = _GLIBCXX_GUARD_WAITING_BIT;
        int old = __atomic_exchange_n (gi, 0, __ATOMIC_ACQ_REL);

        if ((old & waiting_bit) != 0)
            syscall (SYS_futex, gi, _GLIBCXX_FUTEX_WAKE, INT_MAX);
        return;
    }

    set_init_in_progress_flag(g, 0);
}

__cxa_guard_release基本一致,不同的地方在于会将guard变量置0

递归初始化调用

由于在C++11标准中,初始化如果发生了递归是未定义行为,所以GCC 7.3.0针对是否为多线程环境做了不同的处理。如果是多线程环境,不进行额外处理,会发生死锁;如果是单线程环境,则会抛异常。

// acquire() is a helper function used to acquire guard if thread support is
// not compiled in or is compiled in but not enabled at run-time.
static int
acquire(__guard *g)
{
    // Quit if the object is already initialized.
    if (_GLIBCXX_GUARD_TEST(g))
        return 0;

    if (init_in_progress_flag(g))
        throw_recursive_init_exception();

    set_init_in_progress_flag(g, 1);
    return 1;
}

总结

看到了GCC如此复杂的实现,我的个人感想是还是不要自己造轮子来保证单例类的线程安全了,想要做到和GCC一样的高效还是比较难的,利用C++11标准的带来的便利就挺好。

C++11中静态局部变量初始化的线程安全性-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
static关键字是C, C++都存在的关键字, 它主要有三种使用方式, 其前两种只指在C语言使用, 第三种在C++使用(C,C++具体细微操作不尽相同, 本文以C++为准). (1)局部静态变量 (2)外部静态变量/函数 (3)静态数据成员/成员函数 下面就这三种使用方式及注意事项分别说明 一、局部静态变量 在C/C++, 局部变量按照存储形式可分为三种auto, static, register ( 谭浩强, 第174-175页) 与auto类型(普通)局部变量相比, static局部变量有三点不同 1. 存储空间分配不同 auto类型分配在栈上, 属于动态存储类别, 占动态存储区空间, 函数调用结束后自动释放, 而static分配在静态存储区, 在程序整个运行期间都不释放. 两者之间的作用域相同, 但生存期不同. 2. static局部变量在所处模块在初次运行时进行初始化工作, 且只操作一次 3. 对于局部静态变量, 如果不赋初值, 编译期会自动赋初值0或空字符, 而auto类型的初值是不确定的. (对于C++的class对象例外, class的对象实例如果不初始化, 则会自动调用默认构造函数, 不管是否是static类型) 特点: static局部变量的”记忆”与生存期的”全局” 所谓”记忆”是指在两次函数调用时, 在第二次调用进入时, 能保持第一次调用退出时的值. 示例程序一 #include using namespace std; void staticLocalVar() { static int a = 0; // 运行期时初始化一次, 下次再调用时, 不进行初始化工作 cout < < "a= " < (影印版)第103-105页) 下面针对示例程序二, 分析在多线程情况下的不安全.(为方便描述, 标上行号) ① const char * IpToStr(UINT32 IpAddr) ② { ③ static char strBuff[16]; // static局部变量, 用于返回地址有效 ④ const unsigned char *pChIP = (const unsigned char *)&IpAddr; ⑤ sprintf(strBuff, "%u.%u.%u.%u ", pChIP[0], pChIP[1], pChIP[2], pChIP[3]); ⑥ return strBuff; ⑦ } 假设现在有两个线程A,B运行期间都需要调用IpToStr()函数, 将32位的IP地址转换成点分10进制的字符串形式. 现A先获得执行机会, 执行IpToStr(), 传入的参数是0x0B090A0A, 顺序执行完应该返回的指针存储区内容是:”10.10.9.11”, 现执行到⑥时, 失去执行权, 调度到B线程执行, B线程传入的参数是0xA8A8A8C0, 执行至⑦, 静态存储区的内容是192.168.168.168. 当再调度到A执行时, 从⑥继续执行, 由于strBuff的全局唯一, 内容已经被B线程冲掉, 此时返回的将是192.168.168.168字符串, 不再是10.10.9.11字符串. 二、外部静态变量/函数 在Cstatic有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。, 但为了限制全局变量/函数的作用域, 函数或变量前加static使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。注意此时, 对于外部(全局)变量, 不论是否有static限制, 它的存储区域都是在静态存储区, 生存期都是全局的. 此时的static只是起作用域限制作用, 限定作用域在本模块(文件)内部. 使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件的函数同名。 示例程序三: //file1.cpp static int varA; int varB; extern void funA() { …… } static void funB() { …… } //file2.cpp extern int varB; // 使用file1.cpp定义的全局变量 extern int varA; // 错误! varA是static类型, 无法在其他文件使用 extern vod funA(); // 使用file1.cpp定义的函数 extern void funB(); // 错误! 无法使用file1.cpp文件static函数 三、静态数据成员/成员函数(C++特有) C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数. 这是与普通成员函数的最大区别, 也是其应用所在, 比如在对某一个类的对象进行计数时, 计数生成多少个类的实例, 就可以用到静态数据成员. 在这里面, static既不是限定作用域的, 也不是扩展生存期的作用, 而是指示变量/函数在此类的唯一. 这也是”属于一个类而不是属于此类的任何特定对象的变量和函数”的含义. 因为它是对整个类来说是唯一的, 因此不可能属于某一个实例对象的. (针对静态数据成员而言, 成员函数不管是否是static, 在内存只有一个副本, 普通成员函数调用时, 需要传入this指针, static成员函数调用时, 没有this指针. ) 请看示例程序四( (影印版)第59页) class EnemyTarget { public: EnemyTarget() { ++numTargets; } EnemyTarget(const EnemyTarget&) { ++numTargets; } ~EnemyTarget() { --numTargets; } static size_t numberOfTargets() { return numTargets; } bool destroy(); // returns success of attempt to destroy EnemyTarget object private: static size_t numTargets; // object counter }; // class statics must be defined outside the class; // initialization is to 0 by default size_t EnemyTarget::numTargets; 在这个例子, 静态数据成员numTargets就是用来计数产生的对象个数的. 另外, 在设计类的多线程操作时, 由于POSIX库下的线程函数pthread_create()要求是全局的, 普通成员函数无法直接做为线程函数, 可以考虑用Static成员函数做线程函数
Hello World -- 您的第一个程序 6 C# 程序的一般结构 8 Main() 和命令行自变量 9 命令行自变量 10 显示命令行自变量 12 使用 foreach 存取命令行自变量 13 Main() 传回值 14 数据型别 15 在变量宣告指定型别 16 转型和型别转换 21 Boxing 和 Unboxing 24 使用 as 和 is 运算符进行安全转型 27 将字节数组转换为 int 29 将 string 转换为 int 30 在十六进制字符串和数字型别间转换 32 数组 34 将数组当做对象 35 一维数组 36 多维数组 36 不规则数组 37 在数组上使用 foreach 39 传递数组当做参数 40 使用 ref 和 out 传递数组 42 隐含型别数组 44 字符串 45 字符串基本概念 46 串连多个字符串 53 修改字符串内容 56 比较字符串 60 分割字符串 65 使用字符串方法搜寻字符串 66 使用正则表达式搜寻字符串 67 判断字符串是否表示数值 70 将 String 转换为 DateTime 71 在旧版编码方式和 Unicode 间转换 72 转换 RTF 为纯文本 74 语句、表达式和运算符 75 语句 76 表达式 81 运算符 83 匿名函式 86 Lambda 表达式 88 在查询使用 Lambda 表达式 92 在 LINQ 之外使用 Lambda 表达式 94 匿名方法 94 可多载的运算符 97 转换运算符 98 使用转换运算符 99 在结构之间实作用户定义的转换 101 使用运算符多载建立复数类别 103 覆写 Equals() 和运算符 == 的方针 105 类别和结构 108 类别 112 对象 115 结构 118 使用结构 119 继承 122 多型 126 使用 Override 和 New 关键词进行版本控制 132 了解使用 Override 和 New 关键词的时机 135 覆写 ToString 方法 137 抽象和密封类别以及类别成员 138 定义抽象属 140 静态类别和静态类别成员 144 成员 148 存取修饰词 149 字段 151 常数 153 在 C# 定义常数 155 属 156 使用属 157 接口属 165 非对称存取子的存取范围 168 宣告和使用读取/写入属 173 自动实作的属 176 使用自动实作的属来实作轻量型类别 176 方法 177 传递参数 181 传递实值型别的参数 181 传递参考型别的参数 184 了解传递结构和传递类别参考给方法之间的差异 187 隐含型别局部变量 188 在查询表达式使用隐含型别局部变量和数组 191 扩充方法 192 实作和呼叫自定义扩充方法 197 建立列举型别的新方法 199 建构函式 200 使用建构函式 201 实例建构函式 204 私用建构函式 209 静态建构函式 211 撰写复制建构函式 213 对象和集合初始化表达式 217 初始化对象但不呼叫建构函式 219 使用集合初始化表达式来初始化字典 220 嵌套类型 221 部分类别和方法 222 限制 224 匿名型别 227 在查询传回项目属的子集 229 界面 230 明确界面实作 232 明确实作接口成员 234 使用继承明确实作接口成员 236 索引器 239 使用索引器 240 界面的索引器 244 属与索引器之间的比较 246 使用委派 250 使用具名和匿名方法委派的比较 253 使用委派取代接口的时机 255 委派的 Covariance 和 Contravariance 256 组合委派 (多播委派) 258 宣告、产生和使用委派 259 事件 264 订阅及取消订阅事件 265 发行符合 .NET Framework 方针的事件 267 在衍生类别引发基类事件 271 实作界面事件 276 使用字典储存事件实例 280 实作自定义事件存取子 283 泛型 284 泛型简介 285 泛型的优点 287 泛型型别参数 289 泛型类别 295 泛型界面 298 泛型方法 304 泛型和数组 306 泛型委派 307 泛型程序代码的默认关键词 308 C++ 样板和 C# 泛型之间的差异 309 运行时间的泛型 310 .NET Framework 类别库的泛型 311 泛型和反映 312 泛型和属 313 泛型型别的变异数 314 LINQ 查询表达式 325 查询表达式基本概念 328 在 C# 撰写 LINQ 查询 336 查询对象集合 339 从方法传回查询 341 将查询的结果储存在内存 343 使用各种不同方式分组结果 344 将群组包含在群组 352 针对分组作业执行子查询 353 在运行时间动态指定述词筛选条件 362 执行内部联结 364 执行群组联结 372 执行左外部联接 376 排序 Join 子句的结果 378 使用复合索引键执行联结 381 执行自定义联结作业 382 处理查询表达式的 Null 值 387 处理查询表达式的例外状况 388 Iterator 390 使用 Iterator 392 建立整数清单的 Iterator 区块 394 建立泛型清单的 Iterator 区块 395 命名空间 398 使用命名空间 399 使用命名空间别名限定符 403 使用 My 命名空间 405 可为 Null 的型别 407 使用可为 Null 的型别 409 Box 处理可为 Null 的型别 413 识别可为 Null 的型别 414 从 bool? 安全转型至 bool 415 Unsafe 程序代码和指标 415 固定大小缓冲区 416 使用 Windows ReadFile 函式 417 指标型别 421 指标转换 422 指标表达式 424 取得指针变量值 424 取得变量地址 425 使用指标存取成员 426 使用指针存取数组元素 428 管理指标 429 递增和递减指标 429 指标的算术运算 430 指标比较 431 使用指针复制字节数组 432 XML 文件批注 434 建议使用的文件批注标签 435 处理 XML 档案 448 文件标签的分隔符 453 使用 XML 文件功能 454 应用程序域 458 在其他应用程序域执行程序代码 459 建立和使用应用程序域 461 组件和全局程序集缓存 461 Friend 组件 462 判断档案是否为组件 465 加载和卸除组件 466 与其他应用程序共享程序集 466 使用属 468 明示属目标 470 使用反映存取属 472 使用属建立 C/C++ 等位 475 常见属 476 全局属 479 集合类别 483 使用 foreach 存取集合类别 484 使用例外状况 489 例外处理 492 建立和掷回例外状况 495 编译程序所产生的例外状况 498 使用 try/catch 处理例外状况 498 使用 finally 执行清除程序代码 499 拦截非 CLS 例外状况 501 文件系统和登录 502 逐一查看目录树状结构 502 取得档案、文件夹和磁盘驱动器的信息 509 建立档案或文件夹 509 写入文本文件 515 从文本文件读取 516 一次一行读取文本文件 (Visual C#) 516 在登录建立机码 (Visual C#) 517 写入应用程序事件记录文件 (Visual C#) 518 互操作 518 使用平台调用播放 WAV 檔 520 范例 COM 类别 523 线程 524 使用线程 525 线程同步处理 526 建立和结束线程 530 同步处理产生者和消费者线程 534 使用线程集区 542 反映 545 C# DLL 547 建立和使用 C# DLL 547 安全 550
1.static有什么用途?(请至少说明两种) 1)在函数体,一个被声明为静态的变量在这一函数被调用过程维持其值不变。 2) 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。 3) 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用 2.引用与指针有什么区别? 1) 引用必须被初始化,指针不必。 2) 引用初始化以后不能被改变,指针可以改变所指的对象。 3) 不存在指向空值的引用,但是存在指向空值的指针。 3.描述实时系统的基本特 在特定时间内完成特定的任务,实时与可靠。 4.全局变量和局部变量在内存是否有区别?如果有,是什么区别? 全局变量储存在静态数据库,局部变量在堆栈。 5.什么是平衡二叉树? 左右子树都是平衡二叉树 且左右子树的深度差值的绝对值不大于1。 6.堆栈溢出一般是由什么原因导致的? 没有回收垃圾资源。 7.什么函数不能声明为虚函数? constructor函数不能声明为虚函数。 8.冒泡排序算法的时间复杂度是什么? 时间复杂度是O(n^2)。 9.写出float x 与“零值”比较的if语句。 if(x>0.000001&&x<-0.000001) 10.Internet采用哪种网络协议?该协议的主要层次结构? Tcp/Ip协议 主要层次结构为: 应用层/传输层/网络层/数据链路层/物理层。 11.Internet物理地址和IP地址转换采用什么协议? ARP (Address Resolution Protocol)(地址解析協議) 12.IP地址的编码分为哪俩部分? IP地址由两部分组成,网络号和主机号。不过是要和“子网掩码”按位与上之后才能区分哪些是网络位哪些是主机位。 13.用户输入M,N值,从1至N开始顺序循环数数,每数到M输出该数值,直至全部输出。写出C程序。 循环链表,用取余操作做 14.不能做switch()的参数类型是: switch的参数不能为实型。 1.写出判断ABCD四个表达式的是否正确, 若正确, 写出经过表达式 a的值(3分) int a = 4; (A)a += (a++); (B) a += (++a) ;(C) (a++) += a;(D) (++a) += (a++); a = ? 答:C错误,左侧不是一个有效变量,不能赋值,可改为(++a) += a; 改后答案依次为9,10,10,11 2.某32位系统下, C++程序,请计算sizeof 的值(5分). char str[] = “http://www.ibegroup.com/” char *p = str ; int n = 10; 请计算 sizeof (str ) = ?(1) sizeof ( p ) = ?(2) sizeof ( n ) = ?(3) void Foo ( char str[100]){ 请计算 sizeof( str ) = ?(4) } void *p = malloc( 100 ); 请计算 sizeof ( p ) = ?(5) 答:(1)17 (2)4 (3) 4 (4)4 (5)4 3. 回答下面的问题. (4分) (1).头文件的 ifndef/define/endif 干什么用?预处理 答:防止头文件被重复引用 (2). #i nclude 和 #i nclude “filename.h” 有什么区别? 答:前者用来包含开发环境提供的库头文件,后者用来包含自己编写的头文件。 (3).在C++ 程序调用被 C 编译器编译后的函数,为什么要加 extern “C”声明? 答:函数和变量被C++编译后在符号库的名字与C语言的不同,被extern "C"修饰的变 量和函数是按照C语言方式编译和连接的。由于编译后的名字不同,C++程序不能直接调 用C 函数。C++提供了一个C 连接交换指定符号extern“C”来解决这个问题。 (4). switch()不允许的数据类型是? 答:实型 4. 回答下面的问题(6分) (1).Void GetMemory(char **p, int num){ *p = (char *)malloc(num); } void Test(void){ char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); } 请问运行Test 函数会有什么样的结果? 答:输出“hello” (2). void Test(void){ char *str = (char *) malloc(100); strcpy(str, “hello”); free(str); if(str != NULL){ strcpy(str, “world”); printf(str); } } 请问运行Test 函数会有什么样的结果? 答:输出“world” (3). char *GetMemory(void){ char p[] = "hello world"; return p; } void Test(void){ char *str = NULL; str = GetMemory(); printf(str); } 请问运行Test 函数会有什么样的结果? 答:无效的指针,输出不确定 5. 编写strcat函数(6分) 已知strcat函数的原型是char *strcat (char *strDest, const char *strSrc); 其strDest 是目的字符串,strSrc 是源字符串。 (1)不调用C++/C 的字符串库函数,请编写函数 strcat 答: VC源码: char * __cdecl strcat (char * dst, const char * src) { char * cp = dst; while( *cp ) cp++; /* find end of dst */ while( *cp++ = *src++ ) ; /* Copy src to end of dst */ return( dst ); /* return dst */ } (2)strcat能把strSrc 的内容连接到strDest,为什么还要char * 类型的返回值? 答:方便赋值给其他变量 6.MFCCString是类型安全类么? 答:不是,其它数据类型转换到CString可以使用CString的成员函数Format来转换 7.C++为什么用模板类。 答:(1)可用来创建动态增长和减小的数据结构 (2)它是类型无关的,因此具有很高的可复用。 (3)它在编译时而不是运行时检查数据类型,保证了类型安全 (4)它是平台无关的,可移植 (5)可用于基本数据类型 8.CSingleLock是干什么的。 答:同步多个线程对一个数据类的同时访问 9.NEWTEXTMETRIC 是什么。 答:物理字体结构,用来设置字体的高宽大小 10.程序什么时候应该使用线程,什么时候单线程效率高。 答:1.耗时的操作使用线程,提高应用程序响应 2.并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求。 3.多CPU系统,使用线程提高CPU利用率 4.改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独 立的运行部分,这样的程序会利于理解和修改。 其他情况都使用单线程。 11.Windows是内核级线程么。 答:见下一题 12.Linux有内核级线程么。 答:线程通常被定义为一个进程代码的不同执行路线。从实现方式上划分,线程有两 种类型:“用户级线程”和“内核级线程”。 用户线程指不需要内核支持而在用户程序 实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度 和管理线程的函数来控制用户线程。这种线程甚至在象 DOS 这样的操作系统也可实现 ,但线程的调度需要用户程序完成,这有些类似 Windows 3.x 的协作式多任务。另外一 种则需要内核的参与,由内核完成线程的调度。其依赖于操作系统核心,由内核的内部 需求进行创建和撤销,这两种模型各有其好处和缺点。用户线程不需要额外的内核开支 ,并且用户态线程的实现方式可以被定制或修改以适应特殊应用的要求,但是当一个线 程因 I/O 而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不 到运行的机会;而内核线程则没有各个限制,有利于发挥多处理器的并发优势,但却占 用了更多的系统开支。 Windows NT和OS/2支持内核线程。Linux 支持内核级的多线程 13.C++什么数据分配在栈或堆,New分配数据是在近堆还是远堆? 答:栈: 存放局部变量,函数调用参数,函数返回值,函数返回地址。由系统管理 堆: 程序运行时动态申请,new 和 malloc申请的内存就在堆上 14.使用线程是如何防止出现大的波峰。 答:意思是如何防止同时产生大量的线程,方法是使用线程池,线程池具有可以同时提 高调度效率和限制资源使用的好处,线程池的线程达到最大数时,其他线程就会排队 等候。 15函数模板与类模板有什么区别? 答:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化 必须由程序员在程序显式地指定。 16一般数据库若出现日志满了,会出现什么情况,是否还能使用? 答:只能执行查询等读操作,不能执行更改,备份等写操作,原因是任何写操作都要记 录日志。也就是说基本上处于不能使用的状态。 17 SQL Server是否支持行级锁,有什么好处? 答:支持,设立封锁机制主要是为了对并发操作进行控制,对干扰进行封锁,保证数据 的一致和准确,行级封锁确保在用户取得被更新的行到该行进行更新这段时间内不 被其它用户所修改。因而行级锁即可保证数据的一致又能提高数据操作的迸发。 18如果数据库满了会出现什么情况,是否还能使用? 答:见16 19 关于内存对齐的问题以及sizof()的输出 答:编译器自动对齐的原因:为了提高程序的能,数据结构(尤其是栈)应该尽可能 地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问 ;然而,对齐的内存访问仅需要一次访问。 20 int i=10, j=10, k=3; k*=i+j; k最后的值是? 答:60,此题考察优先级,实际写成: k*=(i+j);,赋值运算符优先级最低 21.对数据库的一张表进行操作,同时要对另一张表进行操作,如何实现? 答:将操作多个表的操作放入到事务进行处理 22.TCP/IP 建立连接的过程?(3-way shake) 答:在TCP/IP协议,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。   第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状 态,等待服务器确认; 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个 SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;   第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1) ,此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 23.ICMP是什么协议,处于哪一层? 答:Internet控制报文协议,处于网络层(IP层) 24.触发器怎么工作的? 答:触发器主要是通过事件进行触发而被执行的,当对某一表进行诸如UPDATE、 INSERT 、 DELETE 这些操作时,数据库就会自动执行触发器所定义的SQL 语句,从而确保对数 据的处理必须符合由这些SQL 语句所定义的规则。 25.winsock建立连接的主要实现步骤? 答:服务器端:socker()建立套接字,绑定(bind)并监听(listen),用accept() 等待客户端连接。 客户端:socker()建立套接字,连接(connect)服务器,连接上后使用send()和recv( ),在套接字上写读数据,直至数据交换完毕,closesocket()关闭套接字。 服务器端:accept()发现有客户端连接,建立一个新的套接字,自身重新开始等待连 接。该新产生的套接字使用send()和recv()写读数据,直至数据交换完毕,closesock et()关闭套接字。 26.动态连接库的两种方式? 答:调用一个DLL的函数有两种方法: 1.载入时动态链接(load-time dynamic linking),模块非常明确调用某个导出函数 ,使得他们就像本地函数一样。这需要链接时链接那些函数所在DLL的导入库,导入库向 系统提供了载入DLL时所需的信息及DLL函数定位。 2.运行时动态链接(run-time dynamic linking),运行时可以通过LoadLibrary或Loa dLibraryEx函数载入DLL。DLL载入后,模块可以通过调用GetProcAddress获取DLL函数的 出口地址,然后就可以通过返回的函数指针调用DLL函数了。如此即可避免导入库文件了 。 27.IP组播有那些好处? 答:Internet上产生的许多新的应用,特别是高带宽的多媒体应用,带来了带宽的急剧 消耗和网络拥挤问题。组播是一种允许一个或多个发送者(组播源)发送单一的数据包 到多个接收者(一次的,同时的)的网络技术。组播可以大大的节省网络带宽,因为无 论有多少个目标地址,在整个网络的任何一条链路上只传送单一的数据包。所以说组播 技术的核心就是针对如何节约网络资源的前提下保证服务质量。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值