c++_狄泰总结经典问题
- 经典问题二:
- a)当程序存在多个对象的时候,如何确定这些对象的析构顺序?
- b)const 关键字能否修饰类的对象,如果可以,有什么特性
- b.1)const 对象
- b.2)const 成员函数
- c)成员函数和成员变量都隶属于具体对象的吗?
- 经典问题三
- a)什么时候需要重载赋值操作符?编译器是否提供默认的赋值操作符?
- b)这个类里面没东西,是否为空?
- c)下面的字符串输出什么?为什么不对?标准库有bug?
- 经典问题五
- a)企业面试题1,编写一个程序判断一个变量是不是指针
- b)企业面试题2:问1)构造函数中抛出异常会怎样?
- linux 内存检测工具 valgrind
- 拾遗:令人迷惑的写法(不常见的写法)
- 写法2:try..catch 将函体分成2部分 + 异常声明
- **案例:用最高效的方式来求 1+2+3+...+N(模板技术)**
1)int f() 与int f(void) 有区别吗? 如果有区别是什么? //面试小问题 要看用什么编译器:如果是用c编译器类型.如果是c++编译器
c 语言:
int f():返回值为int,参数为任意的函数,二义性
int f(void):返回值为int的无参(不接受任何参数)函数,
c++:
返回值是int的无参(不接受任何参数)函数
//c语言的默认类型在c++是不合法的,c++不允许不写返回值类型
c 语言缺省为int类型
2)面试题:引用有自己的存储空间吗?
会的?=>功能像指针?==>本质是指针 ==>占用的空间大小与指针一样
经典问题二:
a)当程序存在多个对象的时候,如何确定这些对象的析构顺序?
1)单个对象创建时的构造函数:(先父母,后客人,再自己),
析构函数与对应的构造函数的调用顺序相反
调用父类构造函数
调用成员变量的构造函数
调用类自身的构造函数
2)多个不同的析构顺序:析构顺序与构造顺序 相反
eg:构造顺序 ABC => 析构顺序 CBA
3)栈对象和全局对象,类似于入栈和出栈,最后构造的顺序,最新被析构
4)堆对象的析构 发生在delete的时候,与delete的使用有关
b)const 关键字能否修饰类的对象,如果可以,有什么特性
b.1)const 对象
1)可以,对象从某种程度来说也是变量,const关键字能修饰对象
2)const 修饰的对象 为只读对象 ,const 对象 所对应成员变量的值是不能被改变的
3) const 对象 只能调用const 成员函数
4) 只读的意思(在编译时不能被改变,在运行时可以被改变,用指针来改变)
5)在实际工程开发中,只有很少的对象使用const 对象
const Test t(1); //const 对象
b.2)const 成员函数
1)const 对象只能调用const成员函数
2)const 成员函数 =》 被const对象调用
3)const 成员函数里面不能直接改写成员变量的值,m_value ++ 都不可以
4)在拷贝构造函数里面 Test::Test(const Test& t)
Test::Test(const Test& t) // t =>这个引用是const 引用
{
mi = t.get Mi() ,//所以这个getMi 也是需要const定义
=>mi = t.mi
}
5)const 成员函数的定义 :在函数声明/定义的后面加上const
class Test
{
int getMi()const;
}
Test::getMi()const
{
//mi =2 失败,在const成员函数里面不能修改成员函数的值
return Mi
}
c)成员函数和成员变量都隶属于具体对象的吗?
答1: 每一个对象拥有自己独立的属性(成员变量)
答2: 所有的对象共享一套方法(成员函数)(原因:成员函数存放在代码段,代码段不能动态删除的)
答3: 成员函数能够直接访问对象的属性?
问3: 成员函数是怎么分辨是来自哪个具体对象调用的
答4:通过隐藏的this 指针,编译器会向成员函数隐式传递this指针,this指针指向了该具体对象的地址,隐藏的this指针用于表示当前对象
=》static 静态成员函数没有this 指针
经典问题三
a)什么时候需要重载赋值操作符?编译器是否提供默认的赋值操作符?
答1:当用到系统资源的时候(打开问题,在堆内存创建对象,等)时,需要深拷贝,需要重载拷贝构造函数和赋值操作符重载函数,保持逻辑状态一致,指向两个不同的内存地址
答2:编译器提供了默认的赋值操作符,但只完成浅拷贝的动作,浅拷贝后,物理逻辑相同,两个指针均指向同一个地址
Test& opeateor = { const Test& obj} //重载赋值操作符必须有以下条件
{
if (this != &obj
{
//深拷贝
}
return *this
/* 1)返回值为引用 Test& ,便于连续赋值
2)参数为 const Test& 参数为const 引用
3) 判断是否为自复值? if (this != &obj
4)返回 值
*/
}
b)这个类里面没东西,是否为空?
不是,编译器会自动为他提供构造函数 、拷贝构造函数 赋值操作函数 析构函数
class Test
{
}
等价于
clase Test
{
Test(); //构造函数
~ Test(); //析构函数
Test(const Test& t); //拷贝构造函数
Test& opeateor (const Test& ); //赋值操作符重载函数
}
**
c)下面的字符串输出什么?为什么不对?标准库有bug?
答:明明用了c++ string类,却又采用了c语言的用法,混合使用了c和c++,这样最容易出现不可预料的问题 ==>c++开发尽量避开c语言中惯用的编程思想
string对象内部维护了一个指向数据的char*指针,这个指针可能在程序运行中发生改变
案例1:
string s= "12345";
const char *p =s.c_str(); //用了 C语言 ,这里返回c语言方式的字符串
cout << p <<endl;
s.append("abced"); //p成为了字符串
cout << p << endl
应该直接这样用:
string s= "12345";
string p=s;
s. append("abced");
案例2:
const char* p = "12345";
string s= ""
s.reserve(10);
for(int i=0; i<5; i++)
{
s[i] = p[i]; //证明这里成功赋值了
}
if(s.empty() ) //但这里却为空 ,问题还是由于用了混合编程
{
cout << s<<endl;
}
应该直接改成这样:
const string p = "12345";
string s= ""
s.reserve(10);
s =p;
cout << s<<endl;
经典问题五
小结
C++支持变参函数
变参函数无法很好处理对象参数
利用函数模板和变参函数能够判断指针变量
构造函数和析构函数不要抛出异常
a)企业面试题1,编写一个程序判断一个变量是不是指针
指针是变量
指针保存的值:是某个地址
知识点:
1)c++ 支持c语言的变参函数
2)c++编译器匹配的调用优先级:
普通成员函数 > 函数重载 > 变参函数
思路:
1)将一个变量分为 指针和非指针
2) 编写函数:
指针变量调用时 返回true
非指针变量调用时 返回false
代码实现:利用模板的部分特化(指针特化),编译器优先匹配函数模板利用模板的特化
问题2)基本类型没问题,你可以自定义类类型吗?
问题2=》变参函数(C语言的东西)无法解析对象参数,可能造成程序崩溃
进一步挑战: 如何让编译器精确匹配函数,但不进行实际的调用
用到的知识点 c语言 sizeof 宏定义
class Test
{
public:
Test()
{
}
virtual ~Test()
{
}
};
问题1)编写一个程序判断一个变量是不是指针
template
< typename T>
bool IsPtr( T* v) //指针部分特化
{
return true
}
//bool IsPtr(...) // 变参函数 ...代表任意参数 变参函数->c语言的东西
int IsRtr //针对问题2改变类型
{
return false
}
//#define ISPTR(p) (sizeof(IsPtr(p)) == sizeof(char)) //定义的这个宏,判断调用Isptr的类型是不是bool 一个字节 ,在编译阶段
int main
{
//针对变量1编写一个程序判断一个变量是不是指针
int i=0;
int *p =&i; //这里满足了问题1基本类型需求
cout << "p is a pointer: " << IsPtr(p) <<endl;
cout << "i is a pointer: " << IsPtr(i) <<endl;
//针对问题2,基本类型没问题,你可以自定义类类型吗?
Test t;
Test* pt = &t;
//直接这样使用,会系统出错,变参函数(C语言的东西)无法解析对象参数,可能造成程序崩溃
// cout << "pt is a pointer: " << IsPtr(pt) << endl; // true
//cout << "t is a pointer: " << IsPtr(t) << endl; // false
//这里实现只编译不调用,利用了sizeof 判断isptr类型大小,而不使用,而且利用了宏定义,在预处理阶段就替换了
cout << "pt is a pointer: " << ISPTR(pt) << endl; // true
cout << "t is a pointer: " << ISPTR(t) << endl; // false
}
b)企业面试题2:问1)构造函数中抛出异常会怎样?
构造函数抛出异常会:
1)构造函数立即停止
2)无法创建对象
3)不会调用析构函数
4)对象所占用的空间立即收回
=》建议:在构造函数中不要抛出异常?
接着问2):那这些可能出现异常的代码怎么处理
答:利用二阶构造模式,将可能产生异常的代码放在二阶构造函数里面
问3)那析构函数中抛出异常会怎样?
答:析构函数中抛出异常会导致无法晚上释放内存空中,产生内存泄漏的风险
案例:构造函数抛出异常
#include <iostream>
#include <string>
using namespace std;
class Test
{
public:
Test()
{
cout << "Test()" << endl;
throw 0; //这里会立即返回
}
virtual ~Test()
{
cout << "~Test()" << endl;
}
};
int main(int argc, char *argv[])
{
/*//这里先将p指针初始化为1,证明构造函数抛出异常后,对象创建不成功,new不会返回任何值,包括null
*/
Test* p = reinterpret_cast<Test*>(1);
try
{
p = new Test();
}
catch(...) // ...表示捕获任何异常
{
cout << "Exception..." << endl;
}
cout << "p = " << p << endl; //输出p=1,且不会输出析构函数
return 0;
linux 内存检测工具 valgrind
valgrind --tool=memcheck --leak-check=full ./a.out => 做内存方面的检查 ,检测对象: a.out
拾遗:令人迷惑的写法(不常见的写法)
写法1:class用来在模板中定义泛指类型
来源:历史遗留,当时c++ 用class来进行代码复用
缺点:带来了二义性 => 后面更新用 typename(同时typename 可以消除class带来的二义性)
typename 的作用:
a) 在模板定义时表面泛指类型
b)明确告诉编译器其后的标识符为类型
#include <iostream>
#include <string>
using namespace std;
//在古老的代码里可能会出现,历史遗留问题 ,这种容易让人以为这个是 特制类类型
template < class T > //令人迷惑的写法,class用来在模板中定义泛指类型,跟typename一样的效果
class Test
{
public:
Test(T t)
{
cout << "t = " << t << endl;
}
};
template < class T >
void func(T a[], int len)
{
for(int i=0; i<len; i++)
{
cout << a[i] << endl;
}
}
//
//
二义性案例:
int a = 0;
class Test_1
{
public:
static const int TS = 1;
};
class Test_2
{
public:
struct TS
{
int value;
};
};
template
< class T >
void test_class()
{
单纯这样写会产生二义性: TS 会让人觉得是数据类型 或 静态成员变量
错误写法: T::TS * a; // 1. 通过泛指类型 T 内部的数据类型 TS 定义指针变量 a (推荐的解读方式)
// 2. 使用泛指类型 T 内部的静态成员变量 TS 与全局变量 a 进行乘法操作
typename T::TS * a //正确写法用typename 声明 T:TS 是一个类型(类似Test2里的struct TX)而不是静态成员变量
}
int main(int argc, char *argv[])
{
// test_class<Test_1>();
test_class<Test_2>();
return 0;
}
写法2:try…catch 将函体分成2部分 + 异常声明
a)try…catch 将直接将函体分成2部分,分隔正常功能和异常功能代码
b)函数声明和定义时可以直接指出可能抛出的异常声明
=》异常声明成为函数的一部分可以提供代码的可读性
=》函数异常声明是与编译器的一种约定,违反约定将直接导致运行终止
=》可以直接通过异常声明定义无异常函数
#include <iostream>
#include <string>
using namespace std;
/*int func(int i, int j) throw(int) //如果这里声明抛出的是int类型,确抛出char型,相当于违反了
与编译器的约定,编译ok,执行直接异常终止,哪怕后面 catch(...任意类型)
*/
//函数定义时抛出异常声明,抛出的异常类型只能为 int类型和char类型
int func(int i, int j) throw(int, char)
{
if( (0 < j) && (j < 10) )
{
return (i + j);
}
else
{
throw '0';
}
}
void test(int i) try //try..catch 将直接将函体分成2部分,分隔正常功能和异常功能代码
{
cout << "func(i, i) = " << func(i, i) << endl;
}
catch(int i)
{
cout << "Exception: " << i << endl;
}
catch(...)
{
cout << "Exception..." << endl;
}
int main(int argc, char *argv[])
{
test(5);
test(10);
return 0;
}
案例:用最高效的方式来求 1+2+3+…+N(模板技术)
- for循环
2)等差数列
3)模板技术
依赖技术: 类模板技术 ,模板完全特化技术 数值型参数模板
#include <iostream>
#include <string>
using namespace std;
template
< typename T, int N > //数值型模板参数
void func()
{
T a[N] = {0};
for(int i=0; i<N; i++)
{
a[i] = i;
}
for(int i=0; i<N; i++)
{
cout << a[i] << endl;
}
}
template
< int N >
class Sum //递归定义
{
public:
//static const int VALUE =0 (const int VALUE =0)定义一个常量,用static来限定
/*因此要么存储在全局数据区,要么放在符合表 => value值会直接进入符合表,
由于static 修饰,value会存放在全局数据区 => 证明解决
static const int VALUE = Sum<N-1>::VALUE + N; //递归定义 Sum<N-1>::VALUE + N;编译器会先去求Sum<N-1>::VALUE 的值*/
};
template
< >
class Sum < 1 > //sum 模板类的特化实现(完全特化)
{
public:
static const int VALUE = 1;
};
int main()
{
/*这样最高效,只用了一条语句,计算过程在编译阶段,编译完成后,结果就确定了, Sum<10>::VALUE既没有加减乘除操作,也没有函数调用过程
VALUE是一个常量,在编译阶段已经确认了
当遇到Sum<10>::VALUE时,编译器会去template < int N >class Sum 里去求值,后面发现是递归求值,不断往下走,直到遇到完全特化 template< > class Sum < 1 > ,最终求粗这个值,并把这个值放入符合表中
cout << "1 + 2 + 3 + ... + 10 = " << Sum<10>::VALUE << endl; //这里只做了访问常量
cout << "1 + 2 + 3 + ... + 100 = " << Sum<100>::VALUE << endl;
return 0;
}