1.输入一个字符串,将其逆序后输出
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
int main(){
char data[10]={'\0'};
cin.getline(data,10,'\n');
char temp='\0';
for(int i=0,j=strlen(data)-1;i<strlen(data)/2;i++,j--){
temp=data[i];
data[i]=data[j];
data[j]=temp;
}
cout<<data;
return 0;
}
2.C++中有哪4个和类型转换相关的关键字?这些关键字都有什么特点?应该在哪些场合下使用.
C语言中的强制类型转换可以通过形如: (类型) 变量名 形式的语法进行任意的类型转换。 为什么C++还要引入新的4种类型转换呢?
C++ 中新的类型转换控制符可以很好的控制类型转换的过程,允许控制各种类型不同的转换. 还有一点好处是C++的类型转换控制符能告诉程序员或读者我们这个转换的目的是什么,我们只要看一下代码的类型转换控制符,就能明白我们想要达到什么样的目的.
四类类型转换如下:
** 1)static_cast <T*> (content) 静态转换:** 在编译期间处理,它主要用于C++中内置的基本数据类型之间的转换. 但是没有运行时类型的检测来保证转换的安全性.
为什么需要static_cast类型的转换?
- 用于基类和子类之间的指针或引用的转换。这种转换把子类的指针或引用转换为基类表示是安全的;进行下行转换,把基类的指针或引用转换为子类表示时,由于没有进行动态类型检测,所以是不安全的;
- 把void类型的指针转换成目标类型的指针(不安全).
- 用于内置的基本的数据类型之间的转换.
- 把任何类型的表达式转换成void类型的.
注意:static_cast不会转换掉content的const,volatile,__unaligned属性
2)static_const<T>(content):去常转换:* 编译时执行;
它主要作用同一个类型之间的去常和添加常属性之间的转换.不能用做不同的类型之间的转换.它可以把一个不是常属性的转换成常属性的,同时它也可以对一个本是常属性的类型进行去常.
3)dynamic_cast<T>(content) 动态类型转换:* 也是向下安全转换是在运行的时候执行;通常用于基类和派生类之间的转换.转换时会进行类型安全检查。
- 不能用于内置的基本数据类型之间的转换.
- dynamic_cast转换成功的话返回的是类的指针或引用,失败返回null;
- dynamic_cast进行的转换的时候基类中一定要有虚函数,因为只有类中有了虚函数,才说明它希望让基类指针或引用指向其派生类对象的情况,这样才有意义.这是由于运行时类型检查需要运行时类型的信息,而这些信息存储在虚函数表中.
- 在类的转换时,在类层次间进行转换的时候,dynamic_cast和static_cast进行上行转换的时候效果是一样的;但是在进行下行转换的时候,dynamic_cast会进行类型检查所以它更安全.它可以让指向基类的指针转换为指向其子类的指针或是其兄弟类的指针;
4)reinterpret_cast<T>(content)重解释类型转换:* 它有着和C风格强制类型转换同样的功能;它可以转化任何的内置数据类型为其他的类型,同时它也可以把任何类型的指针转化为其他的类型;它的机理是对二进制数据进行重新的的解释,不会改变原来的格式,而static_cast会改变原来的格式;
3.C++中的一个空类,进行sizeof运算的结果是多少?c++中一个空类包含哪些成员?
C++中的一个空类,sizeof的运算结果是1.为什么不是0呢?空类型不包含任何的信息,但是当我们声明该类型的实例的时候,它必须在内存中占用有一定的空间,否则无法使用这些实例,至于占用多少的内存,由编译器决定,一般是1.一个空类一般包括如下的成员函数?
函数类型 | 表示形式 |
---|---|
默认的缺省构造函数 | Empty(){} |
默认的析构函数 | ~Empty(){} |
拷贝构造函数 | Empty(const Empty& empty){} |
赋值运算符函数 | Empty& operator=(const Empty& empty){} |
取址运算符函数 | Empty* operator&(){} |
const属性的取址运算符函数 | const Empty* operator&() const{} |
4.构造函数,缺省构造函数;拷贝构造函数,缺省的拷贝构造函数;析构函数,缺省的析构函数;之间的区别?
1)构造函数存在的必要性: 因为C++程序设计希望像使用基本的数据类型那样使用类;而一般的时候类的数据成员都是私有的,在外部不能访问;所以它要定义一个构造函数来确保它实例化的对象进行初始化.确保一个类的不同对象之间具有自己想要的初始值.
2)什么时候会调用构造函数?
创建对象的时候会被自动调用,且仅被调用一次.对象定义语句,new操作符.
作用:分配资源,为成员变量赋初值,设置对象的初始状态;
对象的创建过程:
step1: 为整个对象分配内存空间
step2: 以构造实参调用构造函数包括下面活动:
a.构造基类部分(先构造基类的部分)
b.构造成员变量(如果有类类型成员变量的时候会先去执行类类型的构造函数)
c.执行构造代码(这个是自己写的构造函数)
3)缺省构造函数都做什么事情?什么时候才会使用缺省构造函数?
当我们没有提供任何的构造函数的时候,系统(编译器)会提供一个缺省构造函数.
缺省的构造函数做的事情:
a.对基本的成员变量,不做初始化.
b.对类类型的成员变量和基类子对象,调用相应类型的缺省构造函数初始化
c.对于已经定义至少一个构造函数的类,无论其构造函数是否带有参数,编译器都不会为其提供缺省构造函数;
缺省的构造函数也叫无参构造函数,但是未必真的没有任何参数,为一个有参构造函数的每个参数提供一个缺省值,同样可以达到无参构造函数的效果;
4)什么是拷贝构造函数?默认的缺省拷贝构造函数都做什么事情?拷贝构造函数的时机?
a.对于普通的内置类型进行赋值的时候,是很简单的.例如int a = 4;int b = a; 由于用户自己定义的类类型复制的时候,就要调用拷贝构造函数.它的参数必须是const 类名& that的构造函数,用于从一个已经定义的对象构造其同类型的副本,即对象的克隆;
b.对于缺省的拷贝构造函数它所做的就是浅拷贝;(一般没有指针成员的时候可以使用)它主要做的事情就是对基本的成员变量,它会逐字节的复制;对类类型成员变量和基类子对象,调用相应类型的拷贝构造函数.
c.拷贝构造函数的时机:
1.用已经定义的对象作为同类型对象的构造实参;(S s1 = s2这是一个特例.)
2.以对象的值的形式向函数传递参数.
3.从函数中返回一个对象的值.
4.某些拷贝构造过程会因编译优化而被省略.
5)什么是赋值运算符函数?什么时候会用到赋值运算符函数?赋值运算符编写的时候要要注意的事项?
a.A& operator=(const A& a);
b.当用一个已经存在的对象赋值给另外一个已经存在的对象时,会调用赋值运算符函数
c.缺省方式的拷贝构造和拷贝赋值,对包括指针在内的基本类型成员变量按字节赋值,导致浅拷贝问题(只拷贝地址,没有拷贝内容);为了获得完整意义上的对象的副本,必须自己定义拷贝构造函数和拷贝赋值,针对指针型成员变量做深拷贝;
d.相对于拷贝构造,拷贝赋值需要做更多的工作;
避免自赋值
分配新资源
拷贝新内容
释放旧资源
返回自引用
尽量复用拷贝构造函数和析构函数中的代码;
拷贝构造:分配新资源,拷贝新内容;
析构函数:释放旧资源;
6)拷贝构造和拷贝赋值总结
a.无论是拷贝构造还是拷贝赋值,其缺省实现对类类型成员变量和基类子对象,对应会调用相应类型的拷贝构造函数和拷贝赋值运算符函数,而不是简单的按字节复制,因此应当尽量避免使用指针型成员变量;
b.尽量通过引用或指针向函数传递对象型参数,既可以降低参数传递的开销,也能减少拷贝构造的机会.
c.处于具体的原因的考虑,确实无法实现完整意义上的拷贝构造和拷贝赋值的时候,将它们私有化,以防误用.
d.如果一个类提供了自定义的拷贝构造函数,就没有理由不提供实现相同逻辑的拷贝赋值运算符函数.
7)析构函数
析构函数每个类成员只有一个,**是在主函数执行完之后再执行的.析构函数不能重载;**在销毁对象时自动被调用,且仅被调用一次;对象离开所用域的时候会调用析构函数,delete操作符也会调用析构函数,释放在对象的构造过程或声明周期内获得的资源,如果一个类没有定义析构函数的时候,那么编译器会为其提供 一个缺省的析构函数;功能如下:
对基本类型的成员变量,什么也不做;
对类类型的成员变量和基类子对象,调用相应类型的析构函数;
析构函数的执行调用顺序:(对象的销毁过程)
执行析构代码;
析构成员变量
析构基类部分
释放整个对象所占用的内存空间
析构和构建的过程是完全相反的,构造的时候一定是先构造基类的部分再来构造自己的部分;
5.实现一个String类,包括构造函数,析构函数,拷贝构造函数,赋值函数等
String类的基本原型:
class String{
public:
String(const char* str=NULL);
String(const String & that);
~String();
String & operator=(const String & that);
const char * c_str() const;//完成和C的字符串兼容
private:
char * m_str;
};
String 类的实现代码:
#include <iostream>
#include <cstring>
using namespace std;
class String{
public:
String(const char * str=NULL){
if(str==NULL){
m_str=new char[1];
*m_str='\0';
}else{
int length=strlen(str);
m_str=new char[length+1];
strcpy(m_str,str);
}
cout<<"call constructor"<<endl;
}
~String(){
if(m_str){
delete[] m_str;
m_str=NULL;
}
cout<<"call deconstructor"<<endl;
}
String (const String & that){
m_str=new char[strlen(that.m_str)+1];
strcpy(m_str,that.m_str);
cout<<"call copy constructor"<<endl;
}
String & operator= (const String & that){
if(this == &that){
return *this;
};
delete[] m_str;
m_str=new char[strlen(that.m_str)+1];
strcpy(m_str,that.m_str);
cout<<"call align operator"<<endl;
return *this;
}
const char* c_str() const{
return m_str;
}
private:
char * m_str;
};
int main(){
String str1="hello world";
String str2=str1;
cout<<str1.c_str()<<endl;
cout<<str2.c_str()<<endl;
String str3("welcom to C++ world");
str2=str3;
cout<<str1.c_str()<<endl;
cout<<str2.c_str()<<endl;
return 0;
}
运行结果:
call constructor
call copy constructor
hello world
hello world
call constructor
call align operator
hello world
welcom to C++ world
赋值运算符函数注意事项:
-
1)是否把返回值类型声明为该类型的引用,并在函数结束前返回实例自身的引用(即*this).只有返回一个引用,才可以允许连续赋值.
-
2)是否把传入的参数的类型声明为常量引用. 如果传入的参数不是引用而是实例对象,那么从行参到实参会调用一次复制构造函数.把参数声明为引用可以避免这样的无谓的消耗,能提高代码的效率.同时,我们在赋值运算符函数内不会改变传入的实例的状态,因此应该传入的引用参数加上const关键字.
-
3)是否释放实例自身已有的内存, 如果我们忘记在分配新内存之前释放自身已有的空间.程序将内存泄漏.
-
4)是否判断传入的参数和当前的实例是不是同一个实例 如果是同一个,则不进行操作直接返回.如果事先不判断就进行赋值,那么释放实例自身的内存的时候就会导致严重的问题;当*this和传入的参数是同一个实例时,那么一番释放了自身的内存,传入参数的内存也同时被释放了,因此再也找不到需要赋值的内容了