C++基础测试题(1)
三、简答题
1、头文件的ifndef/define/endif有什么作用?
头文件中的#ifndef/define/endif预处理指令用于防止头文件的重复包含,具体作用如下:
ifndef:ifndef是if not define的缩写,其作用是判断某个标识符是否已经被定义过,如果没有定义过,则执行接下来的代码。例如:
#ifndef MY_HEADER_FILE_H
#define MY_HEADER_FILE_H
// 头文件内容
#endif
这段代码首先判断标识符MY_HEADER_FILE_H是否已经被定义过,如果没有定义过,则定义该标识符并执行头文件中的代码。这样可以避免同一个头文件被重复包含多次,从而导致编译错误或者链接错误。
define:define用于定义一个标识符或者宏,通常与ifndef配合使用。例如:
#ifndef MY_HEADER_FILE_H
#define MY_HEADER_FILE_H
#define MAX_NUM 100
#endif
这段代码定义了一个名为MAX_NUM的宏,其值为100。由于使用了ifndef/define保护头文件,因此在多个文件中包含该头文件时,MAX_NUM只会被定义一次。
endif:endif用于结束条件编译指令。例如:
#ifndef MY_HEADER_FILE_H
#define MY_HEADER_FILE_H
// 头文件内容
#endif
这段代码中,endif用于结束ifndef/define条件编译指令的块。当标识符MY_HEADER_FILE_H已经被定义过时,该块中的代码将被忽略。
综上所述,头文件中的ifndef/define/endif预处理指令可以避免头文件的重复包含,提高编译效率,也可以定义宏等预处理指令,方便代码的编写和管理。
2、#include <filename.h>
和#include "filename.h"
有什么区别
在C++中,#include指令用于包含头文件。#include指令有两种形式:#include <filename.h>和#include "filename.h"。
#include <filename.h> 这种形式是用来包含标准头文件或者系统提供的头文件,编译器会在系统的默认路径中查找这些头文件。通常这些头文件是由编译器或者操作系统提供的,它们通常存放在系统目录中,例如C:\Windows\System32\ 或者 /usr/include/等等。 示例:
#include <iostream>
#include <vector>
#include "filename.h" 这种形式是用来包含自定义头文件或者用户提供的头文件,编译器会先在当前目录中查找文件,如果找不到,就会在系统的默认路径中查找这些头文件。 示例:
#include "myheader.h"
#include "util/myutil.h"
因此,这两种形式在包含文件时的搜索路径不同,第一种形式用于包含系统提供的头文件,而第二种形式用于包含自定义的头文件。
3、const有什么用途?(请至少说明两种)
const是C++中的一个关键字,它表示常量,即该变量的值在定义后不能被修改。const有以下两种主要用途:
声明常量
const可以用于声明常量,例如:const int MAX_NUM = 100;
这样定义的MAX_NUM是一个常量,其值为100,不能被修改。使用const定义常量的好处是可以提高代码的可读性和可维护性,避免在程序中不小心修改常量的值。
修饰函数参数和返回值 const还可以用于修饰函数的参数和返回值,例如:
int add(const int a, const int b) {
return a + b;
}
这个函数中,参数a和b被声明为const,表示在函数体内不能修改它们的值。这样可以确保函数不会意外地修改传入的参数,从而提高代码的健壮性。
同时,const还可以用于修饰函数的返回值,表示函数返回的值不能被修改,例如:
const int getValue() {
return 100;
}
这个函数返回的值为100,同时被声明为const,表示调用者不能修改函数返回的值。
综上所述,const具有声明常量、修饰函数参数和返回值等多种用途,可以提高代码的健壮性、可读性和可维护性。
4、在C++程序中调用被C编译器编译后的函数,为什么要加extern "C"?
在C++中,函数名会被编译器进行名称修饰(Name Mangling),以便支持函数重载等特性。而在C语言中,函数名不会进行名称修饰。因此,当C++程序调用被C编译器编译后的函数时,需要告诉编译器要使用C语言的命名规则,否则编译器可能无法找到该函数。
为了解决这个问题,可以使用extern "C"关键字来告诉编译器要使用C语言的命名规则,例如:
extern "C" {
void c_function(int arg);
}
这段代码表示声明了一个C语言编译后的函数c_function,可以在C++程序中调用。使用extern "C"可以避免函数名被名称修饰,保证C++程序能够正确地调用C语言编译后的函数。
需要注意的是,当在C++程序中调用被C编译器编译后的函数时,除了要加上extern "C"之外,还需要注意函数的参数类型和返回值类型是否匹配。因为C++和C语言对函数参数的处理方式不同,需要确保参数类型和返回值类型在C++和C语言中是一致的。
5、请简述以下两个for循环的优缺点。
for(i=0;i<N;i++){
if(condition)
DoSomething();
else
DoSomething();
}
if(condition){
for(i=0;i<N;i++)
DoSomething();
}else{
for(i=0;i<N;i++)
DoSomething();
}
第一个for循环的优点是简单直接,可以很容易地实现需要循环执行的语句,并且可以在循环内根据条件进行不同的处理。缺点是每次循环都需要进行一次条件判断,当循环次数很大时,这种方法的效率可能会降低。
第二个for循环的优点是可以避免循环内重复的条件判断,当满足条件时直接进入循环,不满足条件时跳过整个循环。这样可以提高程序的执行效率,尤其是在循环次数较大时。缺点是代码略微复杂一些,需要在循环外部进行一次条件判断,同时在代码量较大时可能会影响代码的可读性。
综上所述,两种循环方法都有其优缺点,具体使用哪种方法需要根据具体的情况进行选择。如果循环次数较少或者条件判断较为简单,可以使用第一个for循环。如果循环次数较大或者条件判断比较复杂,可以考虑使用第二个for循环。
四、有关内存的思考题
第1题代码如下:
void GetMemory(char* p){
p = (char*)malloc(100);
}
void Test(void){
char *str = NULL;
GetMemory(str);
strcpy(str,"hello world");
printf(str);
}
请问运行Test函数会有什么样的结果?
答:运行Test函数会导致未定义的行为,可能会引起程序崩溃。原因是在GetMemory函数中,虽然使用了malloc函数为指针p分配了100字节的内存空间,但是由于GetMemory函数的参数p是按值传递的,所以在函数内部对p的修改并不会影响到函数外部的str指针。因此,在Test函数中,str仍然是一个空指针,不能直接进行字符串操作,会导致程序出现错误。正确的做法应该是将GetMemory函数的参数改为指向指针的指针,或者返回新分配的内存指针,并将其赋值给函数外部的指针变量。
修改:可以修改GetMemory函数的参数类型,将其改为指向指针的指针类型,这样在函数内部对指针的修改可以影响到函数外部的指针变量。
void GetMemory(char** p){
*p = (char*)malloc(100);
}
void Test(void){
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf("%s", str);
free(str); //记得释放内存
}
第2题代码如下:
char *GetMemory(void){
char p[]="hello world";
return p;
}
void Test(void){
char *str = NULL;
str = GetMemory();
print(str);
}
请问运行Test函数会有什么样的结果?
答:运行Test函数会导致未定义的行为,可能会引起程序崩溃。原因是在GetMemory函数中,声明了一个局部变量p,并将其初始化为"hello world"字符串。然后,将p的地址作为返回值返回给函数调用者。但是,由于p是一个局部变量,它的生命周期只在GetMemory函数中存在,当函数返回时,p的内存空间被释放,指向p的指针变量str所指向的内存空间成为野指针,不能进行字符串操作,会导致程序出现错误。
修改:可以修改GetMemory函数,使用malloc函数为字符串分配动态内存,并将字符串拷贝到动态内存空间中,然后返回指向动态内存的指针。
char* GetMemory(void){
char* p = (char*)malloc(sizeof("hello world")); //为字符串分配动态内存
if(p != NULL){ //检查是否分配成功
strcpy(p, "hello world"); //将字符串拷贝到动态内存中
}
return p; //返回指向动态内存的指针
}
void Test(void){
char* str = NULL;
str = GetMemory();
if(str != NULL){ //检查是否分配成功
printf("%s", str);
free(str); //记得释放内存
}
}
第3题代码如下:
void GetMemory2(char **p, int num){
*p = (char*)malloc(num);
}
void Test(void){
char *str = NULL;
GetMemory(&str, 100);
strcpy(str,"hello");
printf(str);
}
请问运行Test函数会有什么样的结果?
答:运行Test函数会输出"hello",因为在GetMemory2函数中,使用二级指针p来获取动态内存的地址,然后将这个地址赋值给一级指针p,这样一级指针指向的地址就是动态内存的地址了。在Test函数中,调用GetMemory2函数,传递指向指针str的指针,这样GetMemory2函数就可以通过p来修改指针str的值,使其指向分配的动态内存。然后,将"hello"字符串拷贝到动态内存中,并输出字符串。因此,程序可以正确输出"hello"。
在GetMemory2函数中,通过malloc函数分配了一段内存,但在程序结束之前没有释放这段内存。如果在程序中多次调用GetMemory2函数分配内存,但是没有释放这些内存,那么就会造成内存泄漏。为避免内存泄漏,需要在程序结束前,对所有通过malloc函数分配的内存进行释放。
第4题代码如下:
void Test(void){
char *str = (char*)malloc(100);
strcpy(str,"hello");
free(str);
if(str!=NULL){
strcpy(str, "world");
printf(str);
}
}
请问运行Test函数会有什么样的结果?
答:在程序运行时,会先分配一段100字节大小的内存给指针str,并将字符串"hello"拷贝到该内存中。接着通过free函数释放这段内存,将指针str的值设置为NULL。
在if语句中,检查str指针的值是否为NULL,实际上str已经被释放并设置为NULL,所以if语句中的条件判断始终为false,不会进入if语句块。因此,程序不会执行strcpy和printf语句,也就不会输出任何内容。
但是,调用free函数只是将动态分配的内存释放回系统,并不会将指针str的值设置为NULL。因此,if语句中的判断条件实际上是无效的,如果在程序中继续使用str指针,就可能会访问到已经释放的内存,导致未定义的行为。
五、编写strcpy函数
已知strcpy函数的原型是char* strcpy(char* strDest, const char* strSrc);
其中strDest
是目的字符串,strSrc
是源字符串。
(1)不调用C/C++
的字符串库函数,编写函数strcpy
(2)strcpy
能把strSrc
的内容复制到strDest
,为什么还要char*
类型的返回值?
答:(1) 以下是一个简单的实现strcpy
的示例代码:
char* strcpy(char* strDest, const char* strSrc) {
if (strDest == nullptr || strSrc == nullptr) {
return nullptr;
}
char* p = strDest;
while ((*p++ = *strSrc++) != '\0');
return strDest;
}
该函数的实现使用了指针遍历字符数组,将源字符串中的字符逐一复制到目标字符串中。当遍历到源字符串的结尾时,将字符'\0'
复制到目标字符串的末尾,以使目标字符串成为一个以'\0'
结尾的字符串。
(2) strcpy
函数的返回值类型为char*
,这是为了支持链式调用。在链式调用中,多个函数的返回值可以作为下一个函数的参数。例如,可以这样使用strcpy
函数:
char str1[20] = "hello";
char str2[20] = "";
strcpy(str2, strcpy(str1, "world"));
cout << str1 << endl; // "world"
cout << str2 << endl; // "world"
在这个例子中,我们把"world"
字符串复制到了str1
中,并把str1
的内容复制到了str2
中。由于strcpy
函数的返回值类型为char*
,所以我们可以使用strcpy(str2, strcpy(str1, "world"))
语句来实现链式调用。这个语句的执行顺序是先执行strcpy(str1, "world")
,然后再执行strcpy(str2, str1)
。最后输出的结果都是"world"
。
六、编写类String的构造函数、析构函数和赋值函数。
已知类String的原型为:
class String{
public:
String(const char *str = NULL);//普通构造函数
String(const String &other);//拷贝构造函数
~String(void);//析构函数
String & operate=(const String &other);//赋值函数
private:
char *m_data;//用于保存字符串
}
请编写String的上述4个函数。
class String {
public:
// 普通构造函数,可以传入一个字符串参数,用于初始化 m_data
String(const char *str = NULL);
// 拷贝构造函数,用于复制一个已存在的 String 对象
String(const String &other);
// 析构函数,用于释放动态分配的内存
~String(void);
// 赋值函数,用于将一个 String 对象的值赋给另一个 String 对象
String &operator=(const String &other);
private:
char *m_data; // 用于保存字符串的指针
};
// 普通构造函数的实现
// 如果传入了字符串参数,就根据该字符串动态分配内存并复制字符串内容,否则初始化为空字符串
String::String(const char *str) {
if (str == NULL) { // 如果传入参数为空,则初始化为空字符串
m_data = new char[1];
*m_data = '\0';
} else { // 否则根据参数字符串长度分配内存并复制字符串内容
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
// 拷贝构造函数的实现,用于复制已存在的 String 对象
String::String(const String &other) {
int length = strlen(other.m_data); // 先获取待复制的对象的字符串长度
m_data = new char[length + 1]; // 根据字符串长度动态分配内存
strcpy(m_data, other.m_data); // 复制字符串内容
}
// 析构函数的实现,用于释放动态分配的内存
String::~String(void) {
delete[] m_data; // 释放动态分配的内存
}
// 赋值函数的实现,用于将一个 String 对象的值赋给另一个 String 对象
String &String::operator=(const String &other) {
if (this == &other) { // 如果是自己给自己赋值,则直接返回当前对象
return *this;
}
delete[] m_data; // 先释放当前对象已有的内存
int length = strlen(other.m_data); // 获取待赋值的对象的字符串长度
m_data = new char[length + 1]; // 根据字符串长度动态分配内存
strcpy(m_data, other.m_data); // 复制字符串内容
return *this; // 返回当前对象
}
C++基础测试题(2)
7、C语言中,switch开关语句可以测试哪些数据类型
在C++中,switch开关语句可以测试以下几种数据类型:
整型数据:可以是char、short、int和long等整型数据类型。
枚举类型:枚举类型是一种整型数据类型,可以用switch语句进行测试。
字符型数据:可以是char类型。
需要注意的是,浮点型数据和指针类型数据不能用于switch语句的测试条件。另外,在C++11中,也可以使用字符串类型的数据作为switch语句的测试条件,但这需要使用新的switch语句形式,即switch语句配合case标签和std::string类型的数据使用。
8、什么是“引用”?声明和使用“引用”要注意哪些问题?
在C++中,“引用”是一种类型,它提供了一种简单而又安全的方法来间接访问其他变量的内容,可以看做是某个变量的别名。引用是一个变量的另一个名称,通过引用,可以在不改变原变量的情况下,直接访问和修改它的值。
引用的声明方式为:类型名& 引用名 = 原变量名;
例如,假设有一个整型变量a,可以通过以下代码定义一个引用变量b来访问a的值:
int a = 10;
int& b = a;
在使用引用时需要注意以下几点:
-
引用必须在定义时进行初始化,一旦引用被初始化后,它将一直引用同一个变量。
-
引用不能引用一个常量或者字面值,因为常量和字面值是不可修改的。
-
引用不能引用一个空指针。
-
引用作为函数参数传递时,可以将参数传递给函数并在函数内部使用它。
-
引用可以用作函数的返回类型,从而返回一个值,这种方式被称为“返回引用”。
总之,在使用引用时,应该保证引用所引用的变量在引用使用期间保持有效,否则会产生不可预测的结果。此外,引用虽然提供了一种方便的方法来直接访问其他变量的值,但也需要注意避免滥用,避免引起代码的可读性和可维护性的问题。
9、将“引用”作为函数参数有哪些特点?
将“引用”作为函数参数的特点如下:
-
可以通过引用直接访问传递给函数的参数的值,从而避免了复制大对象的开销,提高了函数的执行效率。
-
在函数内部对引用所引用的变量进行修改,将直接修改原变量的值,而不是修改复制的副本,因此,使用引用作为函数参数可以实现在函数外部修改函数内部变量的值。
-
引用作为函数参数传递,可以将参数传递给函数并在函数内部使用它,从而实现更加灵活的函数调用方式。
-
引用作为函数参数传递时,函数中对引用所引用的变量进行的修改将对原变量产生影响,因此使用引用作为函数参数可以实现函数返回多个值的效果。
-
在函数声明中使用引用作为函数参数类型时,可以避免函数对参数进行复制,提高了函数执行的效率。
需要注意的是,使用引用作为函数参数时需要保证传递的引用所引用的变量在函数调用期间保持有效,否则可能会引发不可预测的结果。此外,由于使用引用作为函数参数可以在函数内部修改原变量的值,因此在使用引用作为函数参数时需要特别小心,确保不会修改不应该修改的变量。
10、在什么时候需要使用“常引用”?举例说明。
常引用是指在声明引用时,将引用变量声明为const类型的引用,即引用指向的变量不能被修改。常引用的定义方式为:const 类型名& 引用名 = 原变量名;
常引用主要应用于以下几种情况:
- 在函数调用中传递参数:使用常引用可以避免函数内部对参数的修改,同时避免不必要的内存拷贝,提高函数的执行效率。例如:
void func(const int& a) {
// do something
}
- 返回值为const类型的函数:当函数返回一个const类型的值时,可以使用常引用来接收返回值,避免复制整个返回值。例如:
const int& func() {
static const int value = 100;
return value;
}
- 作为类成员函数的参数:在类成员函数中,如果某个参数不会被修改,那么可以使用常引用来传递参数。例如:
class MyClass {
public:
void func(const int& a) {
// do something
}
};
在使用常引用时需要注意以下几点:
-
常引用所引用的变量不能被修改,否则会引发编译错误。
-
常引用可以指向非常量类型的变量,但是不能通过常引用来修改非常量类型的变量。
-
常引用可以指向常量类型的变量,也可以指向非常量类型的临时变量。
总之,常引用主要用于保证代码的安全性和效率,避免不必要的内存拷贝和修改变量的值,提高代码的可维护性和执行效率。
11、“引用”和指针的区别是什么?
引用和指针是C++中两种非常重要的变量类型,它们有以下区别:
-
初始化:指针可以被初始化为nullptr或者指向某个变量的地址,而引用必须在声明时就被初始化,并且必须指向某个已存在的变量。
-
语法:指针使用*和->运算符进行间接引用,而引用使用.和->运算符进行直接访问。
-
重载:可以重载指针运算符,但不能重载引用运算符。
-
空指针:指针可以指向空地址,即nullptr,而引用必须引用一个已经存在的变量。
-
数组:指针可以指向数组的第一个元素,也可以通过指针算术运算访问数组的其他元素,而引用不能直接绑定到数组,必须通过指针来实现。
-
作为函数参数:指针可以被传递为函数参数,并在函数内部修改指向的变量的值,而引用也可以被传递为函数参数,但是在函数内部对引用所绑定的变量进行的修改将直接修改原变量的值。
-
安全性:使用指针时需要注意指针是否为空以及指针是否越界,否则可能会引发程序崩溃或者安全漏洞,而使用引用则不会存在这些问题。
总之,引用和指针都是C++中非常重要的变量类型,各有自己的优缺点,应根据具体的使用场景来选择使用哪种变量类型。
12、结构和联合有何区别?
结构和联合都是C++中用来定义自定义数据类型的关键字,它们之间的区别主要在以下几个方面:
-
内存分配方式不同:结构体中的各个成员变量是按照定义的顺序依次分配内存空间的,每个成员变量都有自己的内存空间。而联合中的所有成员变量共用同一块内存空间,不同成员变量的值互相覆盖。
-
访问成员变量的方式不同:结构体的成员变量可以通过结构体变量名和点操作符访问,而联合的成员变量可以通过联合变量名和点操作符访问,也可以通过成员变量的名字直接访问。
-
占用的内存空间大小不同:结构体的大小是所有成员变量的大小之和,而联合的大小是其所有成员变量中占用空间最大的那个。
-
可以存储的数据类型不同:结构体中的成员变量可以是不同的数据类型,包括内置类型、自定义类型、指针等,而联合的成员变量必须是同一种数据类型。
-
使用场景不同:结构体主要用于定义复杂的数据类型,将不同类型的数据组合在一起形成一个完整的数据类型,常用于数据结构的定义。联合则常用于需要在不同的数据类型之间进行转换时,如联合可以用于将不同的数据类型存储在同一块内存空间中,从而实现数据类型的转换。
总之,结构体和联合都是C++中非常重要的自定义数据类型,各有自己的特点和优缺点,在具体的编程场景中应根据需要来选择使用哪种类型。
13、关于“联合”的题目的输出
#include <iostream>
using namespace std;
union {
int i;
char x[2];
}a;
int main() {
a;
a.x[0] = 10;
a.x[1] = 1;
printf("%d", a.i);
return 0;
}
x64环境输出:266
小端模式(主机字节序):1*256 + 10*1 = 266
14、输出结果
#define DOUBLE(x) x+x
i=5*DOUBLE(5)
i
是多少?
输出:30
15、main函数执行以前,还会执行什么代码?
在程序开始执行 main 函数之前,会进行一些初始化工作,包括以下几个方面:
-
静态变量和全局变量的初始化:所有的静态变量和全局变量会在程序开始执行前进行初始化,静态变量和全局变量在定义时没有显式地初始化时,会被默认初始化为0或者空指针(对于指针类型)。
-
调用__main函数(可选):如果程序使用了C++的静态对象(如全局变量中含有C++对象),则会在main函数执行之前调用__main函数进行C++运行时环境的初始化。不同的编译器可能会提供不同的实现方式,例如某些编译器可能会将所有的全局变量封装成一个类,然后调用该类的构造函数来完成初始化工作。
-
初始化 C/C++ 运行时环境:在 C/C++ 程序中,还需要进行一些初始化工作来保证程序的正常运行,例如初始化标准 I/O 流、设置环境变量、加载动态链接库等。这些工作由操作系统和运行时库(如 glibc)来完成。
总之,在程序开始执行 main 函数之前,会进行一些重要的初始化工作,这些工作是由编译器和操作系统来完成的,程序员一般不需要过多关注。
16、描述内存分配方式及它们的区别?
在计算机程序中,内存分配是指程序在运行时申请一块内存区域来存储数据,这个过程称为内存分配。常见的内存分配方式包括以下几种:
-
静态内存分配(Static Memory Allocation):在程序编译期间就确定了内存的大小和位置,分配的内存空间在程序整个生命周期中都存在。静态内存分配一般用于声明全局变量和静态变量,以及固定大小的数组等。静态内存分配的优点是分配速度快、存储空间不易被释放,但缺点是占用内存资源长期不释放,容易引起内存浪费。
-
自动内存分配(Automatic Memory Allocation):在函数调用时,编译器会为函数内的局部变量分配内存空间,该内存空间会在函数返回时自动释放。自动内存分配一般用于函数中声明的局部变量,以及堆栈(Stack)中的数据等。自动内存分配的优点是分配和释放方便,但缺点是内存空间容易不足,如果内存空间被占满,就会导致栈溢出(Stack Overflow)等问题。
-
动态内存分配(Dynamic Memory Allocation):程序在运行时根据需要向系统申请一块内存区域,使用完毕后再通过释放函数将其还回系统。动态内存分配一般用于需要动态管理内存空间的数据结构,例如链表、树、图等。常用的动态内存分配函数包括 malloc、calloc、realloc 和 free 等。动态内存分配的优点是灵活性高、内存空间利用率高,但缺点是分配和释放内存需要显式调用相应的函数,如果使用不当会导致内存泄漏(Memory Leak)和内存溢出(Memory Overflow)等问题。
-
内存池分配(Memory Pool Allocation):将多个小块的内存空间预先申请好并放在一个内存池中,程序在运行时直接从内存池中分配需要的内存空间,使用完毕后将其放回内存池。内存池分配一般用于需要频繁分配和释放小块内存空间的场合,例如网络服务器、游戏引擎等。内存池分配的优点是提高了内存分配和释放的效率,但缺点是占用一定的内存空间。
以上是常见的内存分配方式及它们的区别,不同的内存分配方式有不同的使用场合,程序员需要根据实际需求选择合适的的内存分配方式。同时,程序员还需要注意以下几点:
-
内存泄漏(Memory Leak):当程序申请的内存空间未被正确释放时,就会导致内存泄漏问题。内存泄漏会使程序占用的内存越来越多,最终导致程序崩溃。程序员需要注意在使用完毕后及时释放内存空间,避免出现内存泄漏问题。
-
指针悬挂(Dangling Pointer):当程序释放了已经使用的内存空间后,指向该内存空间的指针变成悬挂指针,继续使用该指针会导致程序崩溃。程序员需要注意在释放内存空间后将指针设为 NULL,避免出现指针悬挂问题。
-
缓冲区溢出(Buffer Overflow):当程序向已经满了的内存空间中写入数据时,就会导致缓冲区溢出问题。缓冲区溢出会破坏程序的内存结构,可能导致程序崩溃或者被黑客利用进行攻击。程序员需要注意在使用内存空间时确保不会发生缓冲区溢出问题,避免出现安全隐患。
-
内存对齐(Memory Alignment):不同的硬件平台对内存的对齐要求不同,如果程序在一个硬件平台上编译后在另一个硬件平台上运行,可能会出现内存对齐问题。内存对齐会影响程序的性能和正确性,程序员需要注意在编写程序时遵循硬件平台的内存对齐要求。
总之,内存分配是计算机程序中一个非常重要的问题,程序员需要根据实际需求选择合适的内存分配方式,并注意避免出现内存泄漏、指针悬挂、缓冲区溢出和内存对齐等问题。
17、分别写出bool、int、float、指针类型的变量a与“零”的比较语句。
- bool类型变量a与零的比较语句: a == false 或 !a
- int类型变量a与零的比较语句: a == 0
- float类型变量a与零的比较语句: a == 0.0f
- 指针类型变量a与零的比较语句: a == nullptr 或 a == NULL (对于C++11及以上版本,建议使用nullptr代替NULL)
18、请说出const和#define相比,有何优点?
const
和#define
都可以用来定义常量,但它们有不同的使用方式和优点:
-
类型安全性:使用
const
定义常量可以保证类型安全性,因为它具有数据类型,而#define
只是简单的字符串替换,没有类型检查。如果在使用#define
定义常量时不小心给定错误的数据类型或者没有给定数据类型,就会导致程序出现难以调试的问题。 -
可读性:使用
const
定义常量可以提高代码的可读性,因为它可以给常量起一个有意义的名字,并且在代码中反复使用。而使用#define
定义常量时,常量名字没有明确的作用域,难以区分常量和其他代码。 -
符号表:使用
const
定义常量可以让编译器将其加入符号表,方便调试和优化程序。而#define
定义常量时只是简单的文本替换,不会加入符号表。 -
编译时检查:使用
const
定义常量可以在编译时检查常量的类型和值是否正确,而#define
只是简单的字符串替换,不会进行类型和值的检查。
综上所述,相比于#define
,使用const
定义常量具有更好的类型安全性、可读性、符号表支持和编译时检查。因此,建议在C++
中使用const
来定义常量。
19、简述数组与指针的区别?
数组和指针都可以用来表示一系列的连续内存空间,但它们有以下区别:
-
声明方式不同:数组的声明需要指定数组的大小,例如
int arr[5]
表示一个包含5个整数的数组;指针的声明需要指定指针变量指向的类型,例如int *ptr
表示一个指向整数类型的指针变量。 -
内存分配方式不同:数组在定义时会分配一段连续的内存空间,数组名表示这段内存空间的首地址;而指针需要通过赋值操作指向已经分配的内存空间,否则指针是一个野指针。
-
大小和元素访问方式不同:数组的大小是固定的,可以使用数组下标来访问数组中的元素,例如
arr[2]
表示数组 arr 中的第3个元素;而指针指向的内存空间可以是任意大小,可以使用指针运算符*
和++
来访问指针指向的内存空间中的元素,例如*ptr
表示指针ptr
所指向的内存空间中的元素,ptr++
表示指针ptr
向后移动一个元素的大小。 -
用途不同:数组通常用于表示同一类型的一系列数据,例如
int arr[5]
表示包含5个整数的数组;而指针通常用于动态内存分配、函数参数传递等场景中。
综上所述,数组和指针虽然有一些相似之处,但它们的声明方式、内存分配方式、大小和元素访问方式、以及用途都有所不同。理解它们之间的区别对于C++
编程至关重要。
21、改错题
试题1:
void test1(){
char string[10];
char* str1 = "0123456789";
strcpy(string, str1);
在这个代码片段中,strcpy
函数用于将字符串str1
的内容复制到字符数组string
中。然而,问题在于string
数组只分配了10
个字节的空间,而str1
字符串却包含10
个字符加上一个空字符\0
,因此会导致数组越界访问,引发未定义的行为。为了修正这个问题,可以将string
数组的大小增加到至少11
个字节,即可以容纳字符串str1
的所有字符以及空字符\0
。修改后的代码如下所示:
void test1(){
char string[11];
const char* str1 = "0123456789";
strcpy(string, str1);
}
此外,还需要注意将字符串指针str1
声明为const char*
类型,以避免不小心修改字符串的内容。
试题2:
void test2(){
char string[11],str1[11];
int i;
for(i=0;i<10;i++){
str1[i]='a';
}
strcpy(string, str1);
修改:
void test2(){
char string[11],str1[11];
int i;
for(i=0;i<11;i++){//每个都要初始化
str1[i]='a';
}
strcpy(string, str1);
}
试题3:
void test3(char* str1){
char string[10];
if(strlen(str1)<=10){
strcpy(string, str1);
}
}
修改:
void test3(const char* str1) {
static char string[10]; // 声明为静态数组,作用域为整个程序执行期间
size_t len = strlen(str1);
if (len <= 9) { // 检查传入字符串的长度是否超过数组长度
strncpy(string, str1, len); // 使用 strncpy 函数复制字符串
string[len] = '\0'; // 添加字符串终止符
}
}
试题4:
swap(int* p1, int* p2){
int* p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
该代码存在以下问题:
函数未指定返回类型;
在*p = *p1;
语句中,指针 p 没有被初始化,会导致未定义行为。
以下是修改后的代码:
Copy code
void swap(int* p1, int* p2) {
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
在该代码中,定义了一个tmp
变量,用于保存指针p1
指向的值。通过交换p1
和p2
指向的值,实现了两个变量的交换。这个实现中不需要使用另外一个指针p
。
22、求下面函数的返回值
int func(x){
int countx=0;
while(x){
countx++;
x = x & (x-1);
}
return countx;
}
假定x=9999,输出多少?
答:
这个函数的作用是计算二进制表示下数字x中1的个数,也被称为汉明重量。
对于x=9999,它的二进制表示是0b10011100001111
,共有 8 个1。因此,当输入x=9999时,函数func(x)将返回8。
当输入x=9999时,函数func(x)的执行过程如下:
- 在函数内部定义变量countx并初始化为0。
- 进入while循环,只要x不为0就一直执行循环内的操作。
- 在循环内部,每次将countx加1,表示找到了一个二进制位上的1。
- 将x与(x-1)进行按位与运算,相当于将x的二进制表示中最低位的1变为0。这一步操作的原理是:x & (x-1)的结果等于将x的二进制表示中最低位的1变为0之后得到的数字,因为x的二进制表示中最低位的1之后的所有位都与(x-1)的对应位相同,而最低位的1和之后的所有0都会被(x-1)的对应位所覆盖。
- 当x变为0时,while循环结束,此时countx的值就是x的二进制表示中1的个数。
- 函数返回countx的值,即8。 因此,当输入x=9999时,函数func(x)将返回8。
23、链表题:一个链表的结点结构如下:
struct Node{
int data;
Node *next;
};
typdef struct Node Node;
已知链表的头结点head,写一个函数把这个链表逆序。
答:
可以使用迭代或递归的方式将链表逆序。以下分别介绍两种方法。
迭代法 迭代法的基本思路是遍历原链表,将每个结点的next指针指向它的前驱结点。需要用三个指针分别记录当前结点、前驱结点和后继结点。具体实现步骤如下:
void reverseList(Node *&head){
Node *prev = nullptr; // 前驱结点初始为nullptr
Node *cur = head;
while(cur){
Node *next = cur->next; // 记录后继结点
cur->next = prev; // 当前结点的next指针指向前驱结点
prev = cur; // 更新前驱结点
cur = next; // 更新当前结点
}
head = prev; // 最后将头结点指向原链表的最后一个结点
}
递归法 递归法的基本思路是先逆序后面的结点,然后将当前结点的next指针指向它的前驱结点。需要注意的是,在递归的过程中需要记录当前结点和前驱结点,因为递归回溯时需要更新它们的值。具体实现步骤如下:
void reverseList(Node *&head){
if(!head || !head->next) return; // 处理空链表或只有一个结点的链表
Node *prev = nullptr; // 前驱结点初始为nullptr
Node *cur = head;
reverseList(cur->next); // 逆序后面的结点
cur->next->next = cur; // 将当前结点的next指针指向它的前驱结点
cur->next = nullptr; // 将当前结点的next指针设为nullptr
head = cur; // 更新头结点
}
以上两种方法都可以将链表逆序,具体使用哪种方法取决于具体情况。
25、strncpy和memcpy的区别
在C++中,strncpy和memcpy是两个非常常用的函数,它们都是用来复制内存块的。下面是它们的主要区别:
- 参数不同
strncpy的参数包括源字符串、目标字符串、要复制的字节数,它会将源字符串中的前几个字节复制到目标字符串中,并且如果源字符串不足要复制的字节数,就会用空字符('\0')填充剩余的空间。
memcpy的参数包括源内存块、目标内存块、要复制的字节数,它会将源内存块中的前几个字节复制到目标内存块中,没有任何填充。
-
目的不同 strncpy主要是用来复制字符串的,而memcpy则用来复制任意类型的内存块,不一定是字符串。
-
安全性不同 strncpy在复制字符串时,会保证目标字符串有足够的空间,不会发生越界访问。但是,如果源字符串中的字符数量超过了要复制的字节数,strncpy就会用空字符填充剩余的空间,这可能会导致目标字符串没有以空字符结尾,从而造成一些问题。
memcpy则不会检查目标内存块是否有足够的空间,如果要复制的字节数超过了目标内存块的大小,就会导致越界访问,可能会造成程序崩溃或安全漏洞。
因此,在使用这两个函数时,需要根据具体的需求选择合适的函数,并确保输入参数的正确性,避免发生不必要的错误。
26、#ifndef、#define、#endif的作用还有其他的表示法吗?
#ifndef
、#define
、#endif
是C/C++
中用来防止头文件被重复包含的预处理指令,也被称为头文件保护(Header Guard)。
除了常规的#ifndef
、#define
、#endif
外,还有以下两种常见的头文件保护写法:
#pragma once
#pragma once
是一个非标准的预处理指令,但被广泛支持和使用。它的作用和头文件保护一样,用来防止头文件被重复包含。使用#pragma once
可以简化头文件保护的写法,并且可以提高编译速度。
例如:
#pragma once
#include <iostream>
void func();
_H_、_INCLUDED_
这两种头文件保护写法也是比较常见的,它们的命名规则比较类似,以文件名大写加_H_或_INCLUDED_作为宏定义的名称。
例如:
#ifndef EXAMPLE_H_
#define EXAMPLE_H_
#include <iostream>
void func();
#endif // EXAMPLE_H_
#ifndef EXAMPLE_INCLUDED_
#define EXAMPLE_INCLUDED_
#include <iostream>
void func();
#endif // EXAMPLE_INCLUDED_
无论使用哪种头文件保护的写法,其本质作用都是一样的,都是用来防止头文件被重复包含的。选择哪种写法主要看个人的喜好和代码风格。
27、new、delete、malloc、free的关系
new、delete、malloc、free都是在C++和C语言中用来动态分配和释放内存的函数。它们的关系和区别如下:
- new和delete
new和delete是C++语言中的操作符,用于动态分配和释放内存。它们与malloc和free相比,具有以下优点:
new
可以自动计算所需的内存空间大小,而不需要显式指定。new
可以自动调用对象的构造函数来初始化分配的内存,而malloc
不会自动调用构造函数。delete
可以自动调用对象的析构函数来释放内存,并回收系统资源,而free
不会自动调用析构函数。 例如:
// 动态分配一个整型数组
int* arr = new int[10];
// 动态分配一个对象
MyClass* obj = new MyClass();
// 释放动态分配的内存
delete[] arr;
delete obj;
- malloc和free malloc和free是C语言中用于动态分配和释放内存的函数。它们与new和delete相比,具有以下优点:
malloc
可以分配任意大小的内存空间,而new
只能分配固定大小的内存空间。malloc
返回的指针可以隐式地转换为任何其他类型的指针,而new
返回的指针只能显式地转换为相应的类型指针。free
可以释放任何由malloc
分配的内存空间,而delete
只能释放由new
分配的内存空间。 例如:
// 动态分配一个整型数组
int* arr = (int*)malloc(10 * sizeof(int));
// 释放动态分配的内存
free(arr);
需要注意的是,在C++中使用malloc和free并不是一种良好的编程习惯,因为malloc和free不支持自动调用构造函数和析构函数,容易造成内存泄漏和资源泄漏。推荐使用new和delete来进行动态内存分配和释放。
28、在C++中,子类析构时要调用父类的析构函数吗?
在C++中,子类析构时应该调用父类的析构函数。
当一个子类对象被销毁时,它的析构函数会自动被调用。子类的析构函数应该负责释放子类对象所占用的资源,并且应该调用其父类的析构函数来确保所有继承的资源也被正确释放。
父类的析构函数应该是虚函数,这样在通过父类指针删除子类对象时,可以自动调用正确的析构函数。
例如,假设有以下类层次结构:
class A {
public:
virtual ~A() {}
};
class B : public A {
public:
~B() {
// 在子类析构函数中调用父类析构函数
}
};
在子类B的析构函数中应该调用其父类A的析构函数,以确保所有继承的资源被正确释放。可以通过如下方式实现:
class B : public A {
public:
~B() {
// 在子类析构函数中调用父类析构函数
// 这里调用的是父类A的析构函数
// 可以省略this指针
// 调用方式和虚函数一样
A::~A();
}
};
需要注意的是,C++中的构造函数和析构函数调用顺序是相反的,即在对象构造时先调用父类的构造函数再调用子类的构造函数,而在对象析构时先调用子类的析构函数再调用父类的析构函数。这样可以确保资源被正确释放。
29、虚函数有什么作用,举例写代码说明?
虚函数是一种特殊的函数,它能够在运行时动态地确定调用的是哪个函数,而不是在编译时确定。通过将函数声明为虚函数,可以实现多态性,即同一个函数名可以根据不同的对象类型调用不同的函数实现。
具体来说,当一个类中的函数被声明为虚函数时,在派生类中定义相同名称和参数列表的函数时,该函数也将被自动声明为虚函数,无需再次使用virtual关键字进行声明。
下面是一个简单的示例,说明了虚函数的用途:
#include <iostream>
class Animal {
public:
virtual void speak() {
std::cout << "I am an animal." << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "I am a dog." << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "I am a cat." << std::endl;
}
};
int main() {
Animal* animal = new Animal;
Dog* dog = new Dog;
Cat* cat = new Cat;
animal->speak(); // 输出:I am an animal.
dog->speak(); // 输出:I am a dog.
cat->speak(); // 输出:I am a cat.
delete animal;
delete dog;
delete cat;
return 0;
}
在上面的示例中,Animal
类中的speak
函数被声明为虚函数,而派生类Dog
和Cat
中的speak
函数都使用override
关键字进行了覆盖。
在main
函数中,我们创建了Animal
、Dog
和Cat
的实例,然后调用它们的speak
函数。由于speak
函数被声明为虚函数,因此实际调用的函数取决于对象的类型。当我们调用animal->speak()
时,实际调用的是Animal
类中的speak
函数;而当我们调用dog->speak()
时,实际调用的是Dog
类中的speak
函数。这就实现了多态性的效果。
需要注意的是,为了确保虚函数能够正常工作,析构函数应该被声明为虚函数,否则在删除派生类对象时可能会发生未定义的行为。
30、面向对象的三个基本特征,并简单叙述之?
面向对象编程中的三个基本特征是封装、继承和多态。下面对它们进行简单的叙述:
- 封装
封装是面向对象编程的一个基本特征,它将数据和操作数据的函数封装在一个单元内,对外部世界隐藏了数据的实现细节,只公开了对外部世界必要的接口。这种封装可以提高代码的可维护性、可读性和安全性,也可以降低系统的复杂度。
在面向对象编程中,一个类就是一个封装单元,它定义了一组属性(数据成员)和方法(成员函数),这些属性和方法可以被其他类或者程序所使用,但是其他类或程序无法直接访问这些属性和方法的实现细节。
- 继承
继承是面向对象编程中的另一个基本特征,它允许我们在一个类中定义另一个类,从而可以在已有的类基础上构建出新的类。被继承的类称为父类或基类,新的类称为子类或派生类。子类可以重写或增加父类中的属性和方法,从而实现对父类的扩展。
继承可以提高代码的复用性,避免了代码的重复编写,同时也使代码更易于维护和扩展。在继承关系中,子类可以继承父类的属性和方法,并且可以增加自己的属性和方法,从而实现了代码的复用和扩展。
- 多态
多态是面向对象编程的另一个重要特征,它允许我们使用一个基类类型的指针或引用来访问派生类的对象,从而实现同一操作在不同对象上的不同行为。多态可以大大提高程序的灵活性和可扩展性,使代码更加具有可读性和可维护性。
多态有两种实现方式,一种是运行时多态,使用虚函数来实现;另一种是编译时多态,使用函数重载和模板来实现。无论是哪种方式,多态都可以实现同一操作在不同对象上的不同行为,提高了代码的可复用性和可扩展性。
31、重载(overload)和重写(override)的区别?写代码说明?
重载(overload)和重写(override)是面向对象编程中的两个概念,它们有一定的区别。
重载(overload)指的是在同一个作用域中,定义多个名称相同但是参数列表不同的函数。重载函数会根据参数的类型、个数、顺序等不同的特征进行区分,从而可以实现多态性。
下面是一个重载函数的示例:
// 重载函数 sum
int sum(int x, int y) {
return x + y;
}
float sum(float x, float y) {
return x + y;
}
重写(override)指的是在派生类中重新定义基类中已有的虚函数。派生类重新定义的虚函数必须与基类的虚函数具有相同的名称、参数列表和返回类型。当通过指向基类的指针或引用调用这个虚函数时,实际调用的是派生类中重写的虚函数。
下面是一个重写虚函数的示例:
// 基类 Base
class Base {
public:
virtual void func() {
cout << "This is Base::func()" << endl;
}
};
// 派生类 Derived
class Derived : public Base {
public:
void func() override {
cout << "This is Derived::func()" << endl;
}
};
int main() {
// 使用基类指针调用虚函数
Base* p = new Derived();
p->func(); // 输出 "This is Derived::func()"
delete p;
return 0;
}
在上述代码中,Derived
类重写了Base
类中的虚函数func
,并且使用基类指针调用虚函数时,实际上调用的是Derived
类中重写的虚函数func
。
32、多态的作用?用标准C++写出代码?
多态(polymorphism)是面向对象编程中的一个重要概念,指的是同一个类型的对象,在不同的情况下可以有不同的行为。
多态的作用包括:
- 简化代码:多态使得代码更加简洁、易于理解和维护。
- 提高代码的可扩展性和复用性:通过多态,我们可以方便地添加新的子类,从而扩展代码的功能,同时也可以复用已有的代码。
- 实现接口和抽象类:通过多态,我们可以定义抽象类和接口,从而实现更加灵活的设计。 下面是一个使用多态的示例代码:
#include <iostream>
using namespace std;
// 基类 Shape
class Shape {
public:
virtual void draw() = 0;
};
// 派生类 Rectangle
class Rectangle : public Shape {
public:
void draw() override {
cout << "Drawing a rectangle" << endl;
}
};
// 派生类 Circle
class Circle : public Shape {
public:
void draw() override {
cout << "Drawing a circle" << endl;
}
};
int main() {
// 定义 Shape 类型的指针数组
Shape* shapes[2];
shapes[0] = new Rectangle();
shapes[1] = new Circle();
// 调用 draw 函数实现多态
for (int i = 0; i < 2; i++) {
shapes[i]->draw();
}
// 释放内存
for (int i = 0; i < 2; i++) {
delete shapes[i];
}
return 0;
}
在上述代码中,Shape
是一个抽象类,定义了纯虚函数draw
。Rectangle
和Circle
是两个具体的派生类,分别实现了draw
函数。在main
函数中,我们定义了Shape
类型的指针数组,并使用new
运算符动态分配了两个派生类的对象。通过循环调用draw
函数,实现了多态的效果。最后,使用delete
运算符释放了动态分配的内存。
33、当一个类A中没有任何成员变量与成员函数,这时sizeof(A)的值是多少?
如果一个类A中没有任何成员变量与成员函数,那么sizeof(A)的值为1。
这是因为在C++中,每个对象都必须在内存中占有至少1个字节的空间,即使该对象没有任何成员变量和成员函数。这个字节可以用来区分两个不同的空对象。
例如,考虑下面这个空类A:
class A {
};
虽然这个类没有任何成员变量和成员函数,但是它的大小仍然为1。这意味着如果你创建了两个空类的对象,它们在内存中的地址也是不同的,即使它们是同一个类的实例。
需要注意的是,如果一个类A派生自另一个类B,并且B中有虚函数,那么sizeof(A)将大于1。因为此时A类对象需要存储虚函数表指针,这会增加对象的大小。