本系列博客基于董山海的<C和C++程序员面试秘笈>,旨在记录,欢迎交流,可联系 zywang@shu.edu.cn !
第六章:C++面向对象
文章目录
1、面试题1
描述面向对象技术的基本概念,包括:
- 类(class);
- 对象(object):类的实例;
- 消息(message):对象间相互联系和相互作用的方式;
- 类的特性:抽象、继承、封装、重载、多态;
2、面试题2
类的基本概念:
- 一个类是对象的设计蓝图,因为对象是类的实例,只有类设计好了,对象才可以被创建;
- 使用class关键字创建一个类的结构;
- 一个类一旦被声明了,这个类名就成为一个类型名并可以使用它来声明变量;
- C语言是过程式语言,以过程为中心、以算法为驱动;
- C++语言是面向对象的编程方式,以对象为中心,以消息为驱动的编程方式;
3、面试题3
class 和 struct 有什么区别?
有两种情况:
①C语言的 struct 与 C++ 的class的区别:struct 只是一种复杂数据结构类型定义,不能用于面向对象编程;
②C++的 struct 和 class 的区别:对于成员访问权限以及继承方式,class中默认的是 private,而struct中则是public;另外,class还可以用于表示模板类型,struct则是不行的;
4、面试题4
C++类对象的声明
struct Test {
Test(int) {}
Test(){}
void fun(){}
};
void main() {
Test a(1);
a.fun();
Test b;
b.fun();
}
5、面试题5
C++类成员的访问
#define public private
class Animal {
public:
void MakeNoise();
};
int main() {
Animal animal;
//这边是错误的,因为不能调用私有成员函数
animal.MakeNoise();
return 0;
}
6、面试题6
类成员的初始化
#include <iostream>
using namespace std;
class Obj {
public:
Obj(int k) :j(k), i(j) {
//
}
//
void print(void) {
cout << i << endl;
cout << j << endl;
}
private:
int i;
int j;
};
//
int main() {
Obj obj(2);
obj.print();
}
初始化列表的初始化顺序与变量声明的顺序一致,而不是按照出现在初始化列表中的顺序。这里成员 i 比成员 j 先声明,因此正确的顺序是先用 j 对 i 进行初始化,然后用 2 对 j 进行初始化。由于在对 i 进行初始化的时 j 未被初始化, j 的值为随机值,故 i 的值也为随机值;然后用 2 对j进行初始化,j 的值为2。
7、面试题7
静态成员变量的使用:
- 静态成员:被当作该类类型的全局变量;
- 非静态成员:每个类对象都有自己的复制品;
- 静态成员对每个类的类型只有一个复制品。静态成员只有一份,由该类类型的所有对象共享访问。
#include <iostream>
using namespace std;
class Myclass {
public:
Myclass(int a, int b, int c);
void GetNumber();
void GetSum();
private:
int A, B, C, Num;
static int Sum;
};
//
int Myclass::Sum = 0;
//
Myclass::Myclass(int a, int b, int c) {
A = a;
B = b;
C = c;
Num = A + B + C;
Sum = A + B + C;
}
//
void Myclass::GetNumber() {
cout << "Number = " << Num << endl;
}
//
void Myclass::GetSum() {
cout << "Sum = " << Sum << endl;
}
//
void main() {
Myclass M(3, 7, 10), N(14, 9, 11);
M.GetNumber();
N.GetNumber();
M.GetSum();
N.GetSum();
system("pause");
}
细细体会!
8、面试题8
与全局对象相比,使用静态数据成员有什么优势?
- 静态数据成员没有进入程序的全局名字空间,因此不存在程序中其他全局名字冲突的可能性;
- 使用静态数据成员可以隐藏信息。因为静态成员可以是 private 成员,而全局对象不能。
9、面试题9
初始化列表和赋值的区别:
构造函数初始化列表中初始化成员 或者 在构造函数体中对它们赋值,最终目的都是一样的。
不同在于:使用构造函数初始化列表初始化数据成员,没有定义初始化列表的构造函数在构造函数体中对数据成员进行赋值。
而对于 const 和 reference 类型的成员变量,它们只能够被初始化而不能做赋值操作,因此只能用做初始化列表。
还有一种情况:类的构造函数需要调用其基类的构造函数的时候。
#include <iostream>
using namespace std;
class A { //A是父类
private:
int a; //private成员
public:
A(){}
A(int x):a(x) {} //带参数的构造函数对a进行初始化
void printA() { //打印a的值
cout << "a = " << a << endl;
}
};
class B :public A { //B为子类
private:
int b;
public:
B(int x, int y) :A(x) { //需要初始化b以及父类的a
//a=x; //a为private,无法在子类中被访问
//A(X) //调用方式错误,编译错误
b = y;
}
void printB() {
cout << "b = " << b << endl;
}
};
int main() {
B b(2, 3);
b.printA();
b.printB();
system("pause");
return 0;
}
如果在子类的构造函数中需要初始化父类的 private 成员,直接赋值是不行的,只有调用父类的构造函数才能完成对它的初始化。但是在函数体内调用父类的构造函数也是不合法的,只有初始化列表调用子类构造函数。
10、面试题10
静态成员与非静态成员的理解:
- 关于静态成员变量 i 的初始化,可以在类定义外面初始化;可以直接用类名调用;不能受 private 控制符的作用
- 而非静态成员变量,可以在类内部被初始化;’
#include <iostream>
using namespace std;
class test{
public:
static int i;
int j;
//关于静态成员变量i的初始化,可以在类定义外面初始化
//而非静态成员变量,可以在类内部被初始化
test(int a):i(1), j(a) {}
void func1();
static void func2();
};
void test::func1() {
cout << i << "," << j << endl;
}
void test::func2() {
cout << i << "," << j << endl;
}
int main() {
test t(2);
t.func1();
t.func2();
system("pause");
return 0;
}
11、面试题11
main 函数执行前还会执行什么代码?
首先进行全局对象的构造,然后进行 main 函数中,再进行局部对象的构造。
#include <iostream>
using namespace std;
class Test {
public:
Test() {
cout << "constructor of Test" << endl;
}
};
Test a; //全局变量
int main() {
cout << "main() start" << endl;
Test b; //局部变量
system("pause");
return 0;
}
12、面试题12
C++的空类默认会产生哪些类成员函数
C++的空类中,默认会产生默认构造函数、复制构造函数、析构函数、赋值函数以及取值运算。
13、面试题13
构造函数和析构函数是否可以被重载?
构造函数可以被重载,因为构造函数可以有很多个,且可以带参数;
析构函数不可以被重载,因为只能有一个,且不能带参数;
构造函数的互相调用是会产生栈溢出的。
14、面试题14
explicit构造函数的作用
class Test1 {
public:
Test1(int n) { //普通构造函数
num = n;
}
private:
int num;
};
class Test2 {
public:
explicit Test2(int n) {
num = n;
}
private:
int num;
};
int main() {
Test1 t1 = 12; //隐式调用其构造函数,成功
//Test2 t2 = 12; //编译错误,不能隐式调用其构造函数
Test2 t3(12); //可以这样显示调用
return 0;
}
15、面试题15
explicit用于构造函数的作用
#include <iostream>
#include <string>
using namespace std;
class Number {
public:
string type;
Number() :type("void") {}
explicit Number(short) :type("short") {}
Number(int) :type("int") {}
};
void Show(const Number& n) {
cout << n.type;
}
void main() {
short s = 42;
Show(s); //这边s为short类型
}
16、面试题16
C++中虚析构函数的作用是什么?
#include <iostream>
using namespace std;
class Base {
public:
Base() {}; //Base类的构造函数
~Base() { //Base的解析函数
cout << "Output from the destructor of class Base!" << endl;
};
//
virtual void DoSomething() {
cout << "Do something in class Base!" << endl;
};
};
//
class Derived :public Base {
public:
Derived() {}; //Derived的构造函数
~Derived() { //Derived的析构函数
cout << "Output from the destructor of class Derived!" << endl;
}
};
//
int main() {
Derived *pTest1 = new Derived(); //Derived类指针
pTest1->DoSomething();
delete pTest1; //正常释放pTest1的资源
cout << endl;
Base *pTest2 = new Derived();
pTest2->DoSomething();
delete pTest2; //没有正常释放pTest2的资源
system("pause");
return 0;
}
上述代码的 delete pTest1可以正常释放 pTest1 的资源,而代码 delete pTest2 没有正常释放 pTest2 的资源。因为从结果上看,Derivec 类的析构函数并没有被调用。通常情况下,类的析构函数里面都是释放内存资源,而不调用析构函数的话,会造成内存泄露。原因是指针 pTest2 是 Base 类型的指针,释放 pTest2 时,只进行Base类的析构函数。但是若在~Base()之前加入virtual可得:
此时释放指针 pTest2时,由于 Base 的析构函数是 virtual 的,就会先找到并执行 Derived 的析构函数,然后执行 Base 的析构函数,资源才会正常释放,避免了内存泄露。
因此,只有当一个类被用来作为基类的时候,才会把析构函数写成虚函数。
17、面试题17
析构函数的执行顺序与构造函数的执行顺序相反
#include <iostream>
#include <stdio.h>
using namespace std;
class A {
private:
int a;
public:
A(int aa) {
a = aa;
}
~A() {
cout << "Destructor A!" << a << endl;
}
};
class B :public A {
private:
int b;
public:
B(int aa = 0, int bb = 0) :A(aa) {
b = bb;
}
//
~B() {
cout << "Destructor B!" << b << endl;
}
};
void main() {
B obj1(5), obj2(6, 7);
return;
}
18、面试题18
复制构造函数是什么?什么是深复制和潜复制?
复制构造函数是一种特殊的构造函数,它由编译器调用完成一些基于同一类的其他对象的构件及初始化。
如果在类中没有显示地声明一个复制构造函数,那么,编译器会私下里定制一个函数来进行对象之间的位复制。这个隐含的复制构造函数简单的关联了所有的类成员。
在C++中,3种对象需要复制,在这个过程中,复制构造函数将会被调用。
- 一个对象以值传递的方式进入函数体;
- 一个对象以值传递的方式从函数返回;
- 一个对象需要通过另外一个对象进行初始化;
#include <iostream>
using namespace std;
class Test {
public:
int a;
Test(int x) {
a = x;
}
Test(Test &test) { //复制构造函数
cout << "copy constructor" << endl;
a = test.a;
}
};
void fun1(Test test) { //(1)值传递进入函数体
cout << "fun1()..." << endl;
}
Test fun2() { //(2)值传递从函数体返回
Test t(2);
cout << "fun2()..." << endl;
return t;
}
int main() {
Test t1(1);
Test t2 = t1; //(3)用t1对t2做初始化
cout << "before fun1()..." << endl;
fun1(t1);
Test t3 = fun2();
cout << "after fun2()..." << endl;
system("pause");
return 0;
}
19、面试题19
浅复制:让新旧两个对象指向同一个外部的内容;
深复制:为新对象制作了外部对象的独立复制;
#include <iostream>
using namespace std;
class Test {
public:
char *buf;
//不带参数的构造函数
Test(void) {
buf = NULL;
}
//带参数的构造函数
Test(const char* str) {
buf = new char[strlen(str) + 1]; //这边的buf成员
strcpy(buf, str);
}
//
~Test() {
if (buf != NULL) {
delete buf; //释放buf指向的堆内存
buf = NULL;
}
}
};
int main() {
Test t1("hello");
Test t2 = t1; //注意这边调用了默认的复制构造函数
cout << "(t1.buf==t2.buf)?" << (t1.buf == t2.buf ? "yes" : "no") << endl;
system("pause");
return 0;
}
20、面试题20
对复制构造函数的理解
如果用户没有自定义复制构造函数,但是在代码中使用到了复制构造函数,那么编译器就会生成默认的复制构造函数;
如果用户定义了复制构造函数,那么编译器就不会再生成复制构造函数;
如果用户自己定义了一个构造函数,且不是复制构造函数,但是此时代码中用到了复制构造函数,那么编译器还会生成默认的复制构造函数;但是如果没有使用,那么编译器就不会生成默认的复制构造函数。
21、面试题21
对继承类的复制构造函数的理解
前提:如果基类中没有私有成员,即所有的成员都能被派生类访问,则派生类的复制构造函数很容易写。但是如果基类中有私有成员,并且这些私有成员必须在调用派生类的复制构造函数时被初始化。编写继承类的复制函数有一个原则:使用基类的复制构造函数。
#include <iostream>
using namespace std;
class Base {
public:
//默认的普通构造函数
Base() :i(0) {
cout << "Base()" << endl;
}
//普通构造函数
Base(int n) {
cout << "Base(int)" << endl;
}
//复制构造函数
Base(const Base &b) :i(b.i) {
cout << "Base(Base&)" << endl;
}
private:
int i;
};
class Derived :public Base {
public:
//默认的普通构造函数
Derived() :Base(0), j(0) {
cout << "Derived()" << endl;
}
//Derived类的复制构造函数
Derived(int m, int n) :Base(m), j(n) {
cout << "Derived(int)" << endl;
}
//
Derived(Derived &obj) :Base(obj), j(obj.j) {
cout << "Derived(Derived&)" << endl;
}
private:
int j;
};
int main() {
Base b(1);
Derived obj(2, 3);
cout << "-----" << endl;
Derived d(obj);
cout << "--------" << endl;
system("pause");
return 0;
}
22、面试题22
构造函数与赋值函数的区别
- 复制构造是一个对象来初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行 operator=操作;
class A
A a;
A b = a; //复制构造函数调用
A b(a); //复制构造函数调用
赋值函数:
class A
A a;
A b;
b = a; //赋值函数调用
- 一般来说:在数据成员包含指针对象的时候,应付两种不同的需求:一种是复制指针对象,一种是引用指针对象。复制构造函数大多数情况下是复制,赋值函数则是引用对象;
- 实现不一样。复制构造函数首先是一个构造函数,它调用的时候是通过参数传进来的那个对象来初始化产生一个对象。赋值函数则是把一个对象赋值给一个原有的对象。
23、面试题23
编写string类的构造函数、析构函数和赋值函数:
已知 String 的原型为:
#include <iostream>
using namespace std;
class String {
public:
String(const char *str = NULL); //普通构造函数
String(const String &other); //复制构造函数
~String(void); //析构函数
String &operator=(const String &other); //赋值函数
private:
char *m_String; //私有成员,保存字符串
};
#include <iostream>
using namespace std;
class String {
public:
String(const char *str = NULL); //普通构造函数
String(const String &other); //复制构造函数
~String(void); //析构函数
String &operator=(const String &other); //赋值函数
private:
char *m_String; //私有成员,保存字符串
};
//析构函数
//如果类私有成员m_string不为NULL,释放m_string指向的堆内存,并且为了避免野指针,这个指针赋值为NULL
String::~String(void) {
cout << "Destructing" << endl;
//
if (m_String != NULL) {
delete[] m_String;
m_String = NULL;
}
}
//普通的构造函数
String::String(const char *str) {
cout << "Constructing" << endl;
//这里判断了传入的参数是否为NULL。如果是NULL,初始化一个字节的口字符串(包括结束符'\0')
if (str == NULL) {
m_String = new char[1];
*m_String = '\0';
}
else { //如果不是空字符串,就分配足够大小长度的堆内存来保存字符串
m_String = new char[strlen(str) + 1];
strcpy(m_String, str);
}
}
//
String::String(const String &other) {
cout << "Constructing Copy" << endl;
m_String = new char[strlen(other.m_String) + 1];
strcpy(m_String, other.m_String);
}
//赋值函数:首先判断当前对象与引用传递对象是否为同一个对象;
//如果是,不做操作,直接返回;
//否则。先释放当前对象的堆内存,然后分配足够大小长度的堆内存复制字符串
String&String::operator=(const String &other) {
cout << "Operator = Function" << endl;
//
if (this == &other) {
return *this;
}
delete[] m_String;
m_String = new char[strlen(other.m_String) + 1];
strcpy(m_String, other.m_String);
return *this;
}
int main() {
String a("hello");
String b("world");
String c(a);
c = b;
system("pause");
return 0;
}
24、面试题24
了解C++类成员函数的关系
#include <iostream>
using namespace std;
class A {
private:
int num;
public:
A() {
cout << "Default constructor" << endl;
}
//
~A() {
cout << "Desconstructor" << endl;
cout << num << endl;
}
//
A(const A &a) {
cout << "Copy constructor" << endl;
}
//
void operator=(const A&a) {
cout << "Overload operator" << endl;
}
//
void SetNum(int n) {
num = n;
}
};
//
int main() {
A a1; //这边定义了一个对象a1,调用的是默认的构造函数
A a2(a1); //这边用a1初始化一个对象a2,调用的是复制构造函数
A a3 = a1; //这边不是调用赋值函数,这里属于a3的初始化,而不是赋值
//若是赋值函数,必须下面这个形式
/*
A a3;
a3 = a1;
*/
A &a4 = a1; //这边定义a4为a1的一个引用,不调用构造函数或赋值函数
//调用各个对象的SetNUM()成员函数为私有成员num赋值,由于a4为a1的引用,所以a4.SetNum()和a1.SetNum()是一样的
a1.SetNum(1);
a2.SetNum(2);
a3.SetNum(3);
a4.SetNum(4);
system("pause");
return 0;
}
25、面试题25
C++类的临时对象:
#include <iostream>
using namespace std;
class B {
public:
B() {
cout << "default constructor" << endl;
}
//
~B() {
cout << "destructed" << endl;
}
//初始化私有成员data
B(int i):data(i) {
cout << "constructed by parameter" << data << endl;
}
//
private:
int data;
};
B Play(B b) {
return b;
}
int main() {
B t1 = Play(5);
B t2 = Play(t1);
return 0;
}
这里调用 Play() 函数时,有两种参数类型的传递方式:
如果传递的参数是整型数,那么在其函数栈中首先会调用带参数的构造函数,产生一个临时对象,然后返回前(在 return 代码执行时)调用类的复制构造函数,生成临时对象(这样函数返回后主函数中的对象就被初始化了),最后这个临时对象会在函数返回时(在 return 代码执行和)析构。
如果传递的参数是 B 类的对象,那么只有第一步与上面不同。就是首先其函数栈中会首先调用复制构造函数产生一个临时对象,其余步骤完全相同。
26、面试题26
C++静态成员和临时对象:
#include <iostream>
using namespace std;
class human {
public:
human() {
human_num++;
}
//human有一个静态成员变量human_num,每执行一次,普通构造函数 human_num 加1,每执行一次,析构函数 human_num 减1
static int human_num;
//
~human() {
human_num--;
print();
}
//
void print() {
cout << "human num is:" << human_num << endl;
}
};
//
int human::human_num = 0;
//f1()函数使用默认的复制构造函数,而默认的复制构造函数没有对human_num进行处理。
human f1(human x) {
x.print();
return x;
}
//
int main(int argc, char* argv[]) {
human h1; //调用默认的复制构造函数
h1.print();
human h2 = f1(h1); //使用值传递参数的方式调用f1()函数
/*
这边分为三步:
①在 f1() 函数内首先会调用复制构造函数生成一个临时对象,而默认的复制构造函数,没有对 human_num 进行加1,所以这边打印的不变;
②f1()函数内调用复制构造函数,给main的对象 h2 进行初始化(复制为临时对象);
③f1()函数返回后,临时对象发生析构,减1;
*/
h2.print();
system("pause");
return 0;
}
27、面试题27
对C++临时对象的理解:
当程序员之间进行交谈时,经常把仅仅需要一小段时间的变量称为临时变量。例如在下面的 swap() 函数里:
void swap(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
通常称temp为临时变量,但是在C++里,temp根本不是临时变量。实际上,它只是一个承数的局部变量。
真正的临时对象是看不见的,它不会出现在程序代码中。大多数情况下,它会影响程事执行的效率,所以有时想避免临时对象的产生。它通常在以下两种情况下产生:
- 参数按值传递
- 返回值按值传递
#include <iostream>
using namespace std;
class Test {
public:
Test() :num(0) { } //默认构造函数
Test(int number) :num(number) {} //带参数的构造函数
//打印私有成员num
void print() {
cout << "num = " << num << endl;
}
//析构函数,打印this指针和私有成员num
~Test() {
cout << "destructor:this =" << this << ", num =" << num << endl;
}
//
private:
int num;
};
void fun1(Test test) { //参数按值传递
test.print();
}
Test fun2() { //返回值按值传递
Test t(3);
return t;
}
int main(int argc, char* argv[]) {
Test t1(1);
fun1(t1); //对象传入,使用复制构造函数来创建临时变量
fun1(2); //整型数传入,则使用带参数的构造函数创建临时变量
t1 = fun2();
system("pause");
return 0;
}
如何避免临时变量的产生?可以使用按引用传递代替按值传递。可以改fun1()为:
void fun1(Test &test) {
test.print();
}
28、面试题28
函数重载:描述同名函数具有相同或者相似的功能,但数据类型或者是参数不同的函数管理操作,例如,要进行两种不同数据类型的和的操作,在c语言里面需要些两个不同名称的函数来区分。
#include <iostream>
using namespace std;
class Test {
public:
int add(int x, int y) {
return x + y;
}
//
float add(float x, float y) {
return x + y;
}
};
//
int add(int x, int y) {
return x + y;
}
//
float add(float x, float y) {
return x + y;
}
//
int main(int argc, char* argv[]) {
int i = add(1, 2);
float f = add(1.1f, 2.2f);
Test test;
int i1 = test.add(3, 4);
float f1 = test.add(3.3f, 4.4f);
cout << "i = " << i << endl;
cout << "f = " << f << endl;
cout << "i1 = " << i1 << endl;
cout << "f1 = " << f1 << endl;
system("pause");
return 0;
}
C++可以根据传入参数类型和返回类型来区分不同的重载函数。
为什么C++支持重载函数呢?因为,函数名经过 C++ 编译器处理后包含了原函数名、函数参数数量及返回类型信息,而C语言不会对函数名进行处理。
29、面试题29
函数重载的正确声明:
int cals(int,int)
int cals(const int,const int)
//这个错误的,第二个声明中的const修饰词会被忽略
int get()
double get()
//错误,单就函数的返回值而言,不足区分两个函数的重载
int *reset(int *)
double *reset(double *)
extern "C" int compute(int *,int)
extern "C" double compute(double *,double)
//一组重载函数中,只能有一个函数被指定为extern "C"
30、面试题30
重载与覆写:
重载:子类改写了父类的方法,也就是编写一个与已知函数同名但是参数表不同(参数数量或者参类型不同)的方法,它具有所示的特征:
- 方法名必须相同;
- 参数列表必须不相同,与参数列表的顺序无关;
- 返回值类型可以不相同;
覆写:派生类重写基类的虚函数
- 只有虚方法和抽象方法才能够覆写;
- 相同的函数名;
- 相同的参数列表;
- 相同的返回值类型;
重载是一个语法规则,由编译器在编译阶段完成,不属于面向对象的编程;
覆写由运行阶段决定的,是面向对象编程的重要特征;
31、面试题31
对于下面的类MyString,要求重载一些运算符后可以计算表达式 a=b+c
#include <iostream>
using namespace std;
class MyString {
public:
MyString(char *s) { //参数为字符指针的构造函数
str = new char[strlen(s) + 1];
strcpy(str, s);
}
//
~MyString() { //析构函数释放str堆内存
delete[]str;
}
//
MyString &operator=(MyString &string) { //赋值函数,重载 "=" 符号
if (this == &string) {
return *this;
}
//释放内存
if (str != NULL) {
delete[]str;
}
//申请内存
str = new char[strlen(string.str) + 1];
strcpy(str, string.str); //复制字符串内容
return *this;
}
//第一个版本:重载"+"符号(改变被加对象)
MyString &operator+(MyString &string) {
char *temp = str;
str = new char[strlen(temp) + strlen(string.str) + 1];
strcpy(str, temp); //复制第一个字符串
delete []temp;
strcat(str, string.str); //连接第二个字符串
return *this;
}
//第二个版本:重载"+"符号(不改变被加对象)
MyString & operator+(MyString &string) {
MyString *pString = new MyString(""); //堆内存中构造对象
pString->str = new char[strlen(str) + strlen(string.str) + 1];
strcpy(pString->str, str); //复制第一个字符串
strcat(pString->str, string.str); //连接第二个字符串
return *pString; //返回堆中的对象
}
//
void print() {
cout << str << endl;
}
private:
char *str;
};
//第三个版本,MyString类的友员,要求str成员是public访问权限,重载+(不改变被加对象)
/*
MyString & operator+(MyString &left, MyString &right) {
MyString *pString = new MyString("");
pString->str = new char[strlen(left.str) + strlen(right.str) + 1];
strcpy(pString->str, left.str);
strcat(pString->str, right.str);
return *pString;
}
*/
//
int main() {
MyString a("hello");
MyString b("world");
MyString c("");
c = c + a; //先做加法,再赋值
c.print();
c = c + b;
c.print();
c = a + b;
a.print();
c.print();
return 0;
}
第2个版本属于类的成员函数,第3个版本是类的友员函数。注意类的友元函数不能使用私有成员。
32、面试题32
用C++实现一个 String 类,它具有比较、连接、输入、输出功能:
- <、>、==和 != 比较运算符;
- += 连接运算符以及赋值运算符;
- << 输出运算符 以及 >> 输入运算符;
#ifndef STRING_H
#define STRING_H
#include <iostream>
using namespace std;
class String {
public:
String(); //默认构造函数
String(int n, char c); //普通构造函数
String(const char* source); //普通构造函数
String(const String& s); //复制构造函数
String& operator=(char* s); //重载符号 = ,实现字符串赋值
String& operator =(const String&s); //重载符号 = ,实现对象赋值
~String(); //析构函数
char& operator[](int i); //重载[],实现数组运算
const char& operator[](int i) const; //重载[],实现数组运算(对象为常量)
String& operator+=(const String& s); //重载符号 +=,实现字符串相加
String& operator+=(const char* s); //重载符号+=,实现与对象相加
friend ostream& operator<<(ostream&out, String& s); //重载 << ,实现输出流
friend istream& operator >> (istream&in, String& s); //重载 >> ,实现输入流
friend bool operator < (const String& left, const String& right); //重载 <
friend bool operator > (const String& left, const String& right); //重载 >
friend bool operator == (const String& left, const String& right); //重载 ==
friend bool operator !=(const String& left, const String& right); //重载 =
char* getData(); //获得 data 指针
//输入输出流操作符的重载最好声明为友员函数
private:
int size; //data表示的字符串长度
char *data; //指向字符串数据
};
#endif
//默认构造函数,构造空字符串
String::String() {
data = new char[1]; //空字符串中只含有'\0'一个元素
*data = '\0';
size = 0;
}
//普通构造函数
String::String(int n, char c) { //含有n个相同字符的字符串
data = new char[n + 1];
size = n;
char *temp = data; //保存data
while (n--) { //做n次赋值
*temp++ = c;
}
*temp = '\0';
}
//普通构造函数
String::String(const char *source) {
if (source == NULL) { //字符串内容与source相同
data = new char[1]; //source为NULL
*data = '\0'; //将 data 赋为空字符串
size = 0;
}
else {
size = strlen(source); //source 不为NULL
data = new char[size + 1]; //复制 source 字符串
strcpy(data, source);
}
}
//复制构造函数
String::String(const String &s) {
data = new char[s.size + 1]; //字符串内容与对象 s 的相同
strcpy(data, s.data);
size = s.size;
}
//字符 = 重载,目标为字符串
String& String::operator=(char *s) {
if (data != NULL) {
delete []data;
}
size = strlen(s);
data = new char[size + 1];
strcpy(data, s); //复制目标字符串
return *this;
}
//符号 = 重载,目标为String对象
String& String::operator=(const String& s) {
if (this == &s) { //如果对象s就是自己,直接返回*this
return *this;
}
if (data != NULL) { //释放data内存
delete []data;
}
size = strlen(s.data);
data = new char[size + 1]; //分配堆内存
strcpy(data, s.data); //复制对象s的字符串成员
return *this;
}
//析构函数
String::~String() {
if (data != NULL) { //data不为NULL,释放堆内存
delete []data;
data = NULL;
size = 0;
}
}
//[]重载,取数组下标为i的字符元素
char& String::operator [](int i) {
return data[i];
}
//[]重载,这个数组为常数
const char& String::operator[] (int i) const {
return data[i];
}
//+= 重载,连接对象S的字符串成员
String& String::operator+=(const String& s) {
int len = size + s.size + 1;
char *temp = data;
data = new char[len]; //申请足够的堆内存来存放连接后的字符串
size = len - 1;
strcpy(data, temp); //复制原来的字符串
strcat(data, s.data); //连接目标对象内的字符串成员
delete []temp;
return *this;
}
//+= 重载,连接s字符串
String& String::operator+=(const char *s) {
if (s == NULL) {
return *this;
}
int len = size + strlen(s) + 1;
char *temp = data;
data = new char[len]; //申请足够的堆内存来存放连接后的字符串
size = len - 1;
strcpy(data, temp); //复制原来的字符串
strcat(data, s); //连接目标对象内的字符串成员
delete []temp;
return *this;
}
////获取字符串长度
//String::length() {
// return size;
//}
//重载 <<
ostream& operator << (ostream &out, String &s) {
//打印对象s内存字符串成员的所有字符元素
for (int i = 0; i < s.length(); i++) {
out << s[i] << ""; //输出字符串中mei
}
return out;
}
//从输入流接收最多50个字符
istream& operator >> (istream& in, String& s) {
char p[50];
in.getline(p, 50);
s = p; //调用赋值函数
return in;
}
//重载 <
bool operator < (const String& left, const String&right) {
int i = 0;
while (left[i] == right[i] && left[i] != 0 && right[i] != 0) {
i++;
}
return left[i] - right[i] < 0 ? true : false;
}