收集了在面试中遇到的C/C++笔试题,作为笔记复习
一、计算 sizeof 的值
void Func(char str[100]){}
char str[]=”Hello”;
char *p=str;
int n=10;
void *p1=malloc(100);
请计算:
sizeof(str)= 4
sizeof(str)= 6
sizeof( p )=4
sizeof(n)=4
sizeof(p1)=4
二、计算下面几个类的大小
class A {};: sizeof(A) = 1;
class A { virtual Fun(){} };: sizeof(A) = 4(32位机器)/8(64位机器);
class A { static int a; };: sizeof(A) = 1;
class A { int a; };: sizeof(A) = 4;
class A { static int a; int b; };: sizeof(A) = 4;
int B[10];
sizeof(B) = 40; sizeof(B[10]) = 4;
三、【剑指offer】字符串转换为整数数字
1. 功能测试
- 正数/复数/0
2. 边界值测试
- 最大的正整数/最小的负整数(数据上下溢出)
3. 特殊输入测试
-
空字符串“”的处理,返回0,设置非法输入
-
字符串只有符号位的处理,返回0,设置非法输入
-
输入的字符串中有非数字字符,返回0,设置非法输入
4. 代码:
#include <iostream>
#include <string>
using namespace std;
// 67:把字符串转换成整数
class Solution {
public:
/*全局变量*/
enum {kValid,kInvalid}; // 枚举元素(kValid=0,kInvalid=1)
int g_nStatus = kValid; // 标记是否是非法输入
/*功能函数*/
int StrToInt(string str)
{
g_nStatus = kInvalid; // 初始标记为非法输入
long long num = 0; // 存储结果
const char* cstr = str.c_str();// 指向字符数组的指针
// 判断是否是空指针和空字符串""
if( (cstr != NULL) && (*cstr != '\0') ) {
// 处理符号位
int minus = 1;
if(*cstr == '-') {
minus = -1;
cstr++;
} else if(*cstr == '+') {
minus = 1;
cstr++;
}
// 处理其余位
while(*cstr != '\0') {
if(*cstr > '0' && *cstr < '9') {
// string转int类型
g_nStatus = kValid; // 标记为合法输入
num = num*10 + (*cstr -'0'); // string转换为int类型
cstr++;
// 数据上下溢出
if( ((minus>0) && (num > 0x7FFFFFFF)) ||((minus<0) && (num > 0x80000000)) ) {
g_nStatus = kInvalid; // 如果溢出,则标记为非法输入
num = 0;
break;
}
} else {
g_nStatus = kInvalid;
num = 0;
break;
}
}
if(g_nStatus == kValid)
num = num * minus;
}
cout<<(int)num<<endl;
return (int)num;
}
};
int main()
{
string str = "123";
Solution solution;
solution.StrToInt(str);
return 0;
}
四、C++中String类的实现
class String
{
public:
String(); // 默认构造函数
~String(); // 析构函数
String(const char *str = NULL);// 普通构造函数
String(const String &other);// 拷贝构造函数
String & operator = (const String &other);// 赋值函数
private:
char *m_data;// 用于保存字符串
};
请编写String的上述函数:
String::String() {
m_data = new char[1];
*m_data = '\0';
}
String::~String()
{
if(m_data) {
delete []m_data;
m_data = NULL;
}
}
//普通构造函数
String::String(const char *str)
{
if (str == NULL) {
m_data = new char[1];// 得分点:对空字符串自动申请存放结束标志'\0'的,加分点:对m_data加NULL判断
*m_data = '\0';
} else {
int length = strlen(str);
m_data = new char[length + 1];// 若能加 NULL 判断则更好
strcpy(m_data, str);
}
}
//拷贝构造函数
String::String(const String &other)// 得分点:输入参数为const型
{
int length = strlen(other.m_data);
m_data = new char[length + 1];// 若能加 NULL 判断则更好
strcpy(m_data, other.m_data);
}
//赋值函数
String &String::operator = (const String &other) // 得分点:输入参数为const型
{
if (this == &other)//得分点:检查自赋值
return *this;
if (m_data)
delete[] m_data;//得分点:释放原有的内存资源
int length = strlen(other.m_data);
m_data = new char[length + 1];//加分点:对m_data加NULL判断
strcpy(m_data, other.m_data);
return *this;//得分点:返回本对象的引用
}
- 考察重点一,拷贝构造函数的参数
如果对拷贝构造函数理解不到位,将函数做如下声明:String( String other)
那么,考虑如下情况:myString str1("aaa");
与myString str2 = str1;
由于是传值参数,形参复制到实参会调用拷贝构造函数,就会在拷贝构造函数内部调用拷贝构造函数,会形成永无休止的递归调用,导致栈溢出。 - 考察重点二,赋值运算函数
(1) 是否把返回值的类型声明为该类型的引用
(2) 是否在函数结束前返回自身的引用
(3) 传入参数是否声明为常量引
(4) 是否释放实例自身的内存
(5) 是否判断输入参数是否和当前实例是同一个实例 - 考察重点三,考虑异常及安全
如果在赋值函数内部使用delete
释放实例自身内存,再用new
申请新的内存,如果此时内存不足导致new
操作失败,抛出异常导致类实例状态无效。可以采用临时对象和自身实例交换, 通过局部零时对象离开作用域时调用析构函数,进而释放内存:
String & operator =(const myString & other)
{
if( this != &other ) { // 检查自赋值
myString strtemp(other);
char * pTemp = strtemp.m_data;
strtemp.m_data = m_data;
m_data = pTemp;
}
return *this; // 返回本对象的引用
}
总结:
能够准确无误地编写出String类的构造函数、拷贝构造函数、赋值函数和析构函数的面试者至少已经具备了C++基本功的60%以上!
在这个类中包括了指针类成员变量m_data,当类中包括指针类成员变量时,一定要重载其拷贝构造函数、(拷贝)赋值函数和析构函数,这既是对C++程序员的基本要求,也是《Effective C++》中特别强调的条款。
仔细学习这个类,特别注意加注释的得分点和加分点的意义,这样就具备了60%以上的C++基本功!
五、strcpy函数实现
strcpy函数的原型为:char* strcpy(char* _Dest, const char* _Source);
#include <stdio.h>
char* strcpy(char *strDest, const char* strSrc)
{
/*
//检查传入参数的有效性
assert(NULL != _Dest);
assert(NULL != _Source);
*/
char *p=NULL;
if(strDest == NULL || strSrc == NULL) { return NULL; }
p = strDest;
while((*strDest++ = *strSrc ++) != '\0');
return p;
}
int getStrLen(const char* str)
{
int len = 0;
while( *str ++ != '\0') { len ++; }
return len;
}
strcpy
能把strSrc
的内容复制到strDest
,为什么还要char *
类型的返回值?
答:为了实现链式表达式。
例如 int length = strlen( strcpy( strDest, “hello world”) );
六、memcpy函数实现
memcpy
函数的作用
将由src
指向地址为起始地址的连续n个字节的数据复制到以dest
指向地址为起始地址的空间内,函数返回一个指向dest
的指针。
特别说明
- 与
strcpy
相比,memcpy
遇到'\0'
不会结束,而是一定要拷贝完n个字节,所以要指定拷贝的数据长度 memcpy
可以拷贝任何数据类型的对象,如果dest
和src
的指针类型不一样,也需要处理,不能直接++使地址自增(例如:int* p
和
char*q
,p++
指针的值是4个4个加(0,4,8),q++
是1个1个加(0,1,2,3,4))- 如果
dest
本身就有数据,执行memcpy()
之后会覆盖原有的数据,所以src
和dest
所指向的内存区域不能有重叠 - 不能改变形参的值,定义新的临时变量来操作
- 参数提供的地址可能为空
dest
和src
所指向的地址有重叠的情况
内存地址重叠的情况分为两种,第一种是dest
的地址在src
地址的后面,另一种则是dest地址在src地址的前面。
为了处理上面这两种情况,后来又提供了另一个函数memmove
,在不需要保留原来内存区域的数据的时候可以使用memmove
。
代码
void *memcpy(void *dest, const void *src, size_t count)
{
if (dest == NULL || src == NULL || dest <= src + count) return NULL;
char *tmp_dest = dest;
const char *tmp_src = src;
while (count--) *tmp_dest++ = *tmp_src++ ;
return dest;
}
另外,这段代码还可以优化,比如根据CPU的字节长度,把原来一个一个字节拷贝转换为按CPU的长度拷贝,等等。
优化版:
#include<iostream>
using namespace std;
void* mcpy(void *dest, const void *src, size_t count)
{
if (dest == nullptr || src == nullptr) { return nullptr; }
char *pd = (char*)dest;
const char *ps = (char*)src;
//判断是否会发生内存重叠现象
bool flag1 = (pd < ps && pd + count > ps);
bool flag2 = (pd > ps && ps + count > pd);
if (flag1 || flag2) { return nullptr; }
while (count--) { *pd++ = *ps++; }
return dest;
}