C&C++相关问题汇总

本章独立出来,将对C和C++中一些比较重要的问题进行汇总,也是面试中可能会考的,本文将不断的更新。

本文仅供个人记录和复习,不用于其他用途

关键字static在C语言中的作用

  • 在函数体中,一个被声明为静态的变量在这一函数调用过程中维持不变
  • 在模块中(.c文件),被声明为静态的变量可以被模块中所有的函数访问,但不能被模块以外的函数访问。
  • 在模块中,一个被声明为静态的函数只能被同一模块内的其他函数调用

引用与指针的区别

  • 引用必须被初始化,指针不用
  • 引用初始化后不能被改变,指针可以改变所指的内容
  • 不存在指向空值的引用,但存在指向空值的指针

堆栈溢出的原因

一般来说是由没有回收垃圾资源或者深层次的递归调用所导致的。

局部变量能否和全局变量重名

可以,局部变量会屏蔽掉全局变量,使用::就可以调用全局变量。

全局变量能否定义在被多个.c文件包含的头文件中?

全局变量如果不加static放在.h文件中,会在链接的时候出错,但是加上static,它就相当于各自c文件的本地全局变量。注意,只能够有一个C文件对此变量赋初值。

解释堆和栈的区别

  • 栈由系统自动分配,堆需要程序员申请,并指明大小
  • 栈只要剩余空间大于所申请空间,系统就会为程序提供内存,否则将报出栈溢出。而堆则是需要在系统的空闲内存链表中查找,找到一个空间足够的结点,然后删除此结点并将空间赋予堆。多余的空间将重新返回空闲链表
  • 栈是一块连续的内存区域,向着低地址进行扩展,空间较小。而堆则不连续,向着高地址扩展,空间较大
  • 栈由系统分配,效率高,而堆速度较慢,产生的内存碎片多,但是很方便
  • 在函数调用时,第一个入栈的是主函数中的第一条指令的地址,然后是函数的各个参数,参数从右往左入栈。当函数调用结束时,局部变量先出栈,参数后出栈
  • 如果是存储字符,栈上存储的元素会直接进入寄存器,而堆上会先读取到edx中,再读取字符

const的作用

对于函数来说,就是为了保护参数不被改变,并且也不能改变其他的变量。

const和define的区别

  • const常量有数据类型,宏只是简单的文本替换,没有类型安全检查
  • 被const修饰时都受到强制的保护,能够预防意外变动

strcpy

在使用str开头的系列函数时,一定要注意使用的对象。由于这类函数以‘\0’来判断一个字符串结尾,所以在对字符数组使用时,必须要在数组尾部加上‘\0’。另外,在对字符数组赋值时,如果不是以char ch[] = “”的形式,那么数组末尾元素是没有‘\0’的。

new、delete和malloc、free的区别

deletenew对应,会调用相应的析构函数。mallocfree是标准库函数,deletenew是C++的关键字。因此,mallocfree并不能够执行构造函数和析构函数,仅仅只能够分配内存和回收内存。

delete和delete[]

delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数,对应new[]。事实上,对于简单的类型,两者是一样的。只有当自定义复杂的数组时,delete[]会删除整个数组,而delete只会删除一个指针。

什么是引用

引用是某个变量的别名,所有的操作与直接操作变量是相同的效果。声明一个引用时,必须要初始化,而且引用无法再指向其他对象。事实上,引用仅仅是给一个变量起了个名字,并不是重新定义了一个变量,它本身不占用存储单元。

一般注意以下几点:

  • 不能够建立引用数组
  • 不能够建立指向引用的指针
  • 不能够建立指向引用的引用

简而言之,引用不是一个对象。

什么是常引用

我们使用引用时能够提高程序效率,但是也必须保证引用在传递时不能被改变。一般来说,我们在不改变参数的情况下,最好使用常引用:

1
2
3
4
5
string foo();
void bar(string &s);
bar(foo()); //错误
bar(“hello world”); //错误

上面的代码对bar()的两次调用都是错的,因为foo()返回的是临时对象,临时对象都是const类型,字面字符串就更不用说了。我们无法把一个const类型自动转化成非const类型,所以应该把接受的参数改为常引用:

1
void bar(const string &s);

结构体和联合体的区别

  • 它们都是由多个数据成员构成,但是任意时刻,联合体只存在一个成员,而结构体的所有成员都存在。
  • 对联合体的成员赋值时,会重写其他的成员,那么原来的成员就不存在了,而结构体成员的赋值则互不影响。

重载和重写

重载是在一个类中,函数的参数列表不同(参数类型或参数个数),与函数返回值无关
重写发生在两个类中,与多态真正相关。

多态

多态主要是为了调用相同语句时,能够有不同的效果。实现多态需要三个条件:继承、重写、父类指针(引用)指向子类对象。

使用virtual关键字可以重写父类相关的方法,比如我们对父类的某个方法声明virtual,那么子类就可以对其进行重写。需要注意的是,哪怕你不对子类声明virtual,它也是会被默认为是虚函数。当父类指针指向子类对象时,将不再调用父类的相关方法,而是会调用子类方法。

实现虚函数的方法是建立一个虚函数表,类中会生成一个指向虚函数表的指针,里面存储的是子类相关的虚函数。当我们调用父类方法时,会根据虚函数表中的地址,找到子类相关方法,并且调用它们,也就实现了我们需要的多态。具体可以用下面的代码证明:

1
2
3
4
class car
{
virtual void run(){};
};

我们不妨打印一下这个类的大小,会发现输出是4。显然,当我们定义一个虚函数时,会自动生成一个指向虚函数表的指针。另外,虚析构函数的实现也是如此。

C++是不是类型安全

不是,两个不同类型的指针可以强制转换。

main函数之前,会执行什么代码

全局对象的构造会在main函数之前。

描述内存分配方式以及它们的区别

  • 从静态存储区域分配。内存在程序编译时候就已经分配好了,这块内存在程序的整个运行期间都存在,例如全局变量、static变量
  • 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令
  • 从堆上分配。程序在运行期间用mallocnew申请任意多少的内存,程序员自己负责在何时用freedelete释放内存,动态内存的生存期由程序员自己决定

C++的四种类型转换

C风格转换

类似于下面这种:

1
2
3
4
5
int i;
double d;
i = (int)d;
i = int(d);

简单类型确实可以这么做,但是对于复杂的类和类指针,这么随意的转换是不行的,所以C++提供了四种操作符:reinterpret_caststatic_castdynamic_castconst_cast,主要用于类之间的转换。

reinterpret_cast

这个操作符能够在非相关类型之间的转换,操作结果只是简单地从一个指针到另一个指针的二进制拷贝,在类型和所指向的内容不做任何类型的检查和转换。

1
2
3
4
class A {};
class B {};
A *a = new A;
B *b = reinterpret_cast<B *>(a);

其实这个与强制转换没有任何区别,可以直接使用强制转换。

static_cast

这个主要用于强制隐式转换,比如将普通变量转换成常量,或者是int转换成double。不过,它没有办法将常量转换成普通变量。

当它应用于指针上时,它允许父类指针和子类指针相互转化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
class Base
{
public:
virtual void print()
{
cout << "Base";
}
};
class Derived : public Base
{
public:
virtual void print()
{
cout << "Derived";
}
};
int main()
{
Base *b = new Base();
Derived *d = static_cast<Derived *>(b);
d->print();
}

这里父类指针被转换成了子类指针,但是当你调用子类的方法时,显示的依旧是Base,因为父类指针还是指向的Base,然而,static_cast并没有报错。下面这个父类指针接受的才是子类:

1
2
3
Derived *d = new Derived();
Base *b = d;
b->print();

这个时候,父类就会调用子类的函数,因为接受的是子类对象。

你当然可以使用C风格强制转换,只不过不够规范,编译器也不容易识别。

dynamic_cast

dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,简而言之,它用于安全地沿着类的继承关系向下转换。注意,这个转换是安全的,如果失败,那么将返回一个空指针。

使用这个操作符时,父类必须要有虚函数,也就是说,dynamic_cast是针对多态的:

1
2
3
4
5
6
7
8
class Base {virtual dummy() {}};
class Derived : public Base {};
Base *b1 = new Base();
Base *b2 = new Derived();
Derived *d1 = dynamic_cast<Derived *>(b1); // 失败
Derived *d2 = dynamic_cast<Derived *>(b2); // 成功

相比较static_cast的转换,dynamic_cast要安全的多。我之前已经举过例子了,static_cast转换的指针仍然指向的是基类,但是它并没有提醒我们。但如果你使用的是动态检测,那么这个操作就不会成功。

由于是运行时才进行检测,所以要消耗一些资源。另外,加上了引用是一样的效果:

1
2
Derived d1 = dynamic_cast<Derived &*>(b1); // 失败
Derived d2 = dynamic_cast<Derived &*>(b2); // 成功

const_cast

const_cast用于强制消除对象的常量性,其他三种cast是不能对其修改的:

1
2
3
4
class C {};
const C *a = new C;
C *b = const_cast<C *>(a);

总结

  • 消除const属性用const_cast
  • 基本类型转换用static_cast
  • 多态类之间的类型转换用dynamic_cast
  • 不同类型的指针类型转换用reinterpret_cast

extern “C”

在C++程序中调用被C编译后的函数,为什么要加上extern “C”

extern

我们先看一个例子:

1
2
3
4
5
6
7
// file1.c:
int x=1;
int f() {do something here}
// file2.c:
extern int x;
int f();
void g() {x = f();}

file2.c中使用的xf()都是定义在file1.c中,而extern语句仅仅只是一个变量的声明(C++严格区分定义与声明)。由于变量x 是全局变量,可以声明多次(类型必须一致),但是在一个项目中只能被定义一次(除非你定义的是一个局部变量):

1
2
3
4
5
6
7
8
9
// file1.c:
int x=1;
int b=1;
extern c;
// file2.c:
int x;
int f();
extern double b;
extern int c;

上面的代码存在着错误:

  • x被定义了两次
  • b两次被声明为不同的类型
  • c被两次声明但没有被定义

extern关键字起的是声明作用,被声明的函数和变量可以在本模块或者其他模块。注意,static变量或函数只能够本模块中使用,且不能被extern修饰。

那么extern “C”有什么用呢?这个主要是为了C/C++的混合编译,因为不同语言的变量存储不太一样,比如C就没有函数重载。

加上了这条语句后,表明它按照类C的编译和连接规约来编译和连接,如果你不这么写,那么你所写的函数和变量是无法在C中使用的,主要原因还是二者生成的名字不一样,比如C++生成的函数int foo(int i, int j)的名字就可能是_foo_int_int,而C就是_foo

C/C++混合编译

如果要在C++中调用C头文件,那么在C头文件中应该这么编写:

1
2
3
4
5
6
#ifndef C_HEADER
#define C_HEADER
extern void print(int i);
#endif C_HEADER

C语言是不支持extern “C”的,这一点要记住。

.c文件的实现如下:

1
2
3
4
5
6
#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
printf("cHeader %d\n",i);
}

如果要在.cpp文件中调用这个函数,那么就要按照以下格式:

1
2
3
4
5
6
7
8
9
extern "C"{
#include "cHeader.h"
}
int main(int argc,char** argv)
{
print(3);
return 0;
}

刚刚说的是怎么在C++中调用C代码,那么反过来怎么做呢?先来看看C++头文件怎么定义:

1
2
3
4
5
6
#ifndef CPP_HEADER
#define CPP_HEADER
extern "C" void print(int i);
#endif CPP_HEADER

.cpp文件中的实现如下:

1
2
3
4
5
6
7
8
#include "cppHeader.h"
#include <iosteram>
using namespace std;
void print(int i)
{
cout << "cppHeader" << i << endl;
}

.c文件中调用:

1
2
3
4
5
6
extern void print(int i);
int main(int argc,char** argv)
{
print(3);
return 0;
}

总结

如果有下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef MONGOOSE_HEADER_INCLUDED
#define MONGOOSE_HEADER_INCLUDED
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/*.................................
* do something here
*.................................
*/
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* MONGOOSE_HEADER_INCLUDED */

那么最前面的宏定义是防止头文件重复包含,至于下面这个部分:

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/*.................................
* do something here
*.................................
*/
#ifdef __cplusplus
}
#endif /* __cplusplus */

假设我们实现了一个C语言模块,但是不知道是被C使用还是C++时,就可以像上面这么写,这个是标准头文件格式。

如果模块已经写好,并且没有extern “C”,那么可以在你的.cpp文件中这么写:

1
2
3
extern "C" {
#include "test_extern_c.h"
}

如果你只是想用某个函数,可以这么写:

1
2
3
extern "C" {
int ThisIsTest(int, int);
}

然后就可以在模块中使用这个函数。

虚构造函数

虽然有虚析构函数,但是虚构造函数是不可能的。我们知道,因为父类对象接受子类对象时,如果不把父类的析构函数声明为virtual,那么就不会调用子类的析构函数。

既然如此,那么构造函数呢?我们创建子类对象时,会自动调用父类的默认构造函数。如果父类没有默认构造函数,那么就必须要子类自行调用。

如此看来,我们根本就不需要虚构造函数。况且,对象还没有实例化,根本就没有虚函数表,哪里去找虚构造函数?同样,拷贝构造函数也是如此。

C预处理功能

主要有宏定义、文件包含、条件编译。

C++编译器自动产生的类

主要有默认构造函数,拷贝构造函数,析构函数,赋值函数。

拷贝函数在哪几种情况下会被调用

  • 当类的一个对象去初始化该类的另一个对象时
  • 当一个对象做函数的形式参数
  • 当一个对象做函数返回值时

在类外有什么方法可以访问类的非公有成员

友元、继承、公有成员函数。

不允许重载的5个运算符有哪些

  • 成员指针访问运算符*
  • 域运算符::
  • 长度运算符sizeof
  • 条件运算符?:
  • 成员运算符.

流运算符为什么不能通过类的成员函数重载

因为通过类的成员函数重载时,运算符的第一个对象要是自己,而对流运算的重载要求第一个参数是流对象,所以一般通过友元函数来解决。

对象成员进行初始化的次序是什么

由声明的次序决定。

对象间是怎样实现数据的共享的

通过类的静态成员变量来实现对象间的数据共享,静态成员变量占有自己独立的空间不为某个对象所私有。

函数重载与虚函数

函数重载是一个同名函数完成不同的功能,编译系统在编译阶段通过函数参数个数、参数类型不同、函数的返回值来区分该调用哪个函数,即实现的是静态的多态性。但是记住:不能仅仅通过函数返回值不同来实现函数重载。

虚函数实现是在基类中通过使用关键字virtual来申明一个函数为虚函数,含义就是该函数的功能可能在将来的派生类中定义或者在基类的基础上进行扩展,系统只能在运行阶段才能动态决定该调用哪一个函数,所以实现的是动态的多态性。

注意,构造函数可以重载,但是析构函数不行。

static的应用和作用

  • 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用的时候仍然维持上次的值
  • 在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其他函数访问
  • 在模块内的static函数只可以被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的类模块中
  • 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝
  • 在类中的static成员函数属于整个类所拥有,这个函数不接受this指针,因而只能访问类的static成员变量

const的应用和作用

  • 欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对他进行初始化,因为以后就没有机会改变它了
  • 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const
  • 一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值
  • 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能改变类的成员变量
  • 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为左值

如何判断一段程序是由C编写的还是C++编写的

1
2
3
4
5
6
#ifdef __cplusplus
cout << "c++";
#else
printf("c");
#endif

bool、int、float、指针与零值比较

1
2
3
4
5
6
7
8
9
10
11
// bool
if (a)
// int
if (a == 0)
// float
if (a < 0.000001 || a > -0.000001)
// 指针
if (a == NULL)

写一个标准宏来交换参数

1
2
3
4
#define swap(a, b) \
(a) = (a) + (b); \
(b) = (a) - (b); \
(a) = (a) - (b);

位操作交换两个变量的值

1
2
3
a = a ^ b;
b = a ^ b;
a = a ^ b;

要记住,异或运算符^的规则是相异为1,相同为0。假设a的二进制为001,而b的二进制为010,那么计算如下:

  • 第一次计算:a : 011
  • 第二次计算:b : 001
  • 第三次计算:a : 010

实现各类字符串相关函数

strcmp

1
2
3
4
5
6
7
8
9
10
11
12
13
int mystrcmp(char *p1, char *p2) //接受两个字符串比较大小,返回值为0代表相等,为1表示p1>p2,为-1表示p1<p2
{
int i = 0;
while(p1[i] == p2[i] && p1[i] != '\0')
i++;
if(p1[i] == '\0' && p2[i] == '\0')
return 0;
else if(p1[i] > p2[i])
return 1;
else
return -1;
}

strcpy

1
2
3
4
5
6
7
8
9
10
11
char *mystrcpy(char *str,char *message) // 接受两个指向字符的指针,返回一个字符指针
{
char *last = NULL;
if(str == NULL || message == NULL) // 如果两个指针都为空,不必拷贝,直接返回
return last;
last = str; // 将str的首地址赋给last,last是最终的结果
while((*str++ = *message++) != '\0'); // (*str++=*message++)的结果就是*message,如果不为'\0',那么证明字符串没有结束,继续拷贝
return last;
}

strlen

1
2
3
4
5
6
7
8
9
10
11
12
int mystrlen(const char *p) // const表示传入的字符串不可以随便更改,指针用于取出字符
{
int len = 0;
if(p == NULL)
return -1;
else
{
while(*p++)
len++;
}
return len;
}

strncat

1
2
3
4
5
6
7
8
9
10
11
void mystrncat(char *source, char *copy, int n)
{
char *p = source;
int i;
if(source == NULL || copy == NULL)
return;
while(*p != '\0')
p++;
for(i = 0; i <= n; i++)
*p++ = copy[i];
}

strrev

1
2
3
4
5
6
7
8
9
10
11
void mystrrev(char *p)
{
int len = strlen(p);
int i;
for(i = 0; i < len / 2; i++)
{
char ch = p[i];
p[i] = p[len - 1 - i];
p[len - 1 - i] = ch;
}
}

strstr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int strStr(string haystack, string needle)
{
if (needle.empty())
return 0;
int m = haystack.size(), n = needle.size();
if (m < n)
return -1;
for (int i = 0; i <= m - n; ++i)
{
int j = 0;
for (j = 0; j < n; ++j)
{
if (haystack[i + j] != needle[j])
break;
}
if (j == n)
return i;
}
return -1;
}