Fork me on GitHub

《C++ 习题与解析》笔记

C++基础: 基础数据类型,简单输入输出,流程控制语句,函数与编译预处理,数组,结构体,指针与引用
C++面向对象部分: 类与对象,构造和析构函数,继承与派生,友元,虚函数,静态成员,运算符重载

Chapter-1 C++语言概述

位运算操作符
单目:~(按位求反)
双目:&(与)、 |(或)、 ^(按位异或)
移位运算符

<< (左移): 左移是将一个二进制数按指定的位数向左移位,移掉的位被丢弃,右边移出的空位一律补0
>> (右移): 右移是将一个二进制数按指定的位数向右移位,移掉的位被丢弃,左边移出的空位一律补0,或补符号位

逗号运算符

d1, d2, d3, d4: 计算一个逗号表达式的值时,从左至右依次计算各个表达式的值,最后计算的一个表达式的值和类型便是整个逗号表达式的值和类型

二维数组指针表示
	//输出对应的值的三种方法
    int b[2][3]; 
    a. *(*(b+i)+j)
    b. *(b[i]+j)
    c. *(&b[0][0]+3*i+j)
const 位置与指向问题
	//都定义了a是一个常整数,不能修改a的值
	int const a = 1;
	const int a = 1;

	int b[] = {1,2,3}
	const int *a = b; // a为指向常整数的指针,a为数组b的首地址,a指向的整数不可修改,不能通过a来修改其指向的值,但可以通过b来修改数组值,而且a可以指向其他数组
	int * const a = b; // a为指向整数的常指针,a为数组b的首地址,a指向的整数可以修改,但是指针a不可修改,不能让a指向其他数组,也不能执行a++语句
指针传址操作
void swap()
{
	int a = 1,b=2;
	int& a1 = a;
	int& b1 = b;
	swap1(a,b); //传变量的值(在方法内操作的只是temp,改变不了实际ab的值) 
	swap2(&a,&b); //传变量的地址(直接操作内存地址的值,最粗暴的方式 ) 
	swap3(a1,b1); //传变量的引用 (变量与变量的别名,它们的地址是同一个,直接操作ab的值 ) 
	cout<< a << "," << b; 
}

void pointer2(){
    int  a=10;
    int& b =a;
    int  *q;
    q = &a;
    
    //修改a的值 
    *q = 2;
    printf("%d\n",*q);
    changeValue(q);//不行
    changeValue2(&q);//(ok)传二级指针修改值 
    changeValue3(b);//(ok)直接传引用修改值 
    printf("%d\n",*q);
}
void changeValue(int *p){//传进来的p是a的地址,或者说p只是一个普通变量,这个变量存放了a的地址 
    int  b=100;
    p = &b;//因为p是一个变量,所以,这边的p是个副本_p,对副本进行操作,并不会改变实际a的值 
}
void changeValue2(int **p){//传进来的是p的地址,不是a的地址!! 
    int  b=100;
    *p = &b;//这边虽然也有一个副本_p,但是没有影响,因为*p取到p地址的值,也就是a的地址,然后修改其值 
}
void changeValue3(int& p){
    p = 100;//引用理解为是对指针进行了封装,使用起来简单 
}

Chapter-1 C++语言概述(错题)


  • [例1.3]
   int i=1;
   void max()
   { int i = i;} 
   //main()里的i是未定义值,是局部变量,当执行int i=i;时,先在内存中为局部变量i分配内存空间,其值是不确定的,然后执行i=i,由于值不确定,所以main()里的i是一个未定义值
  • [例1.10]
    在一定条件下,指针可以进行两种运算,即两个指针可以相减,一个指针可以与一个适当的整数相加减,但不能进行两个指针的相加运算

  • [例1.16]

	struct
	{
		float f; //4 byte
		char c;  //1 byte
		int adf[3]; //12byte;
	}x;
	cout << sizeof(x) << endl; // output: 20byte;
	//默认以4字节为对齐单位,f占4个字节,c占一个字节,adf占12个字节,总共17个字节。按4字节为对齐单位时,要选择4的倍数,即为20个字节。
	
  • [例1.24]
    swicth(){ case _A_: 表达式。 } //在A处:case之后只能用常量表达式,不能用实型表达式
  • [例1.30]
 for(int i=0,j=10; i=j=10; i++,j--); //执行次数:无限次。 return i=j=10 -> true
  • [例1.36]
    若要定义一个只允许本源文件所有函数使用的全局变量,应该使用的存储类别是:static
    c++规定,全局变量分为两种:extern和static型,前者的作用域为整个程序,后者的作用域为定义该变量的文件
  • [例1.43]
	void main()
	{
		int b = 3;
		int arr[] = {6,7,8,9,10};
		int *ptr = arr;
		*(ptr++) += 123;
		printf(" %d, %d\n",*ptr,*(++ptr));  //output: 8 8 ;
	}

对于printf语句,其参数从右向左运算,第一个是*(++ptr),第二个是*ptr

  • [例1.57]
    func(x)的功能是将x转化为2进制,求其中含有的1的个数
	int func(int x)
	{
		int countx = 0;
		while(x)
		{
			countx++;
			x = x & (x-1);    //每一次计算对应2进制的一个1就会变成0 直到所有1都变为0循环结束
		}
		return countx;
	}

Chapter-2 类和对象


定义类注意点
  1. 类体中不允许对所定义的数据成员进行初始化
  2. 用new创建的类对象都是匿名对象,必须用一个指针指向它,通过该指针对这个对象进行操作 class A{..}; A *p = new A;
拷贝构造函数

用一个已知对象来初始化一个被创建的同类对象

class Sample{
	Sample(const Sample &s){...}
}
void main(){
	Sample s1, s2(s1);
}

使用情况如下:

  • 明确表示由一个对象初始化另一个对象时
  • 当对象作为函数实参传递给函数形参时
  • 当对象作为函数返回值时
调用析构函数

析构函数自动调用的情况

  • 对象被定义在一个函数体内,则当这个函数结束时,该对象的析构函数被自动调用
  • 使用new运算符动态创建一个对象后,当使用delete运算符释放它时,delete将会自动调用析构函数

析构函数的调用顺序与构造函数相反

	void main(){
		Sample s1,s2(10); //析构函数调用时先释放s2,再释放s1
	}
深拷贝/浅拷贝

浅拷贝:当两个对象之间进行复制,复制完成后,还共享某些资源(内存空间),其中一个对象销毁会影响另一个对象,这种对象之间的复制称为对象浅复制

class Sample{
private:	
	int no;
	char *pname;
public:
	Sample(const Sample &s){
		no = s.no;
		pname = s.pname;
	}
}
void main(){
	Sample s1, s2(s1);
}

深拷贝:当两个对象之间进行复制,复制完成后,他们不会共享任何资源,其中一个对象的销毁不会影响另一个对象
种对象之间的复制称为对象深复制

class Sample{
private:	
	int no;
	char *pname;
public:
	Sample(const Sample &s){
		no = s.no;
		// pname = s.pname;
		pname = new char[strlen(s.pname)+1];
		strcpy(pname,s.pname);
	}
}
void main(){
	Sample s1, s2(s1);
}
类成员函数指针
class Sample
{
	int m,n;
public:	
	void setm(int i){  m=i; }
	void setn(int i){  n=i; }
}
void main(){
	void(Sample::*pfun)(int);   //类成员函数指针
	Sample a;
	pfun=Sample::setm;          //指向Sample类成员函数setm;
	(a.*pfun)(10);
	pfun=Sample::setn;          //指向Sample类成员函数setn;
	(a.*pfun)(20);
}

程序中类Sample的setm/setn成员函数必须具有相同的返回类型(这里均为void),而且为public时才能这样使用

子对象

构造函数的执行次序是:(析构函数相反)

  1. 以子对象在类中说明的顺序调用子对象初始化列表中列出的各构造函数A(参数表):obj(参数表2){ 函数体; }
  2. 然后执行函数体
class B1{
public:
	B1(){cout << "B1:Constructor" << endl; }
	~B1(){cout << "B1:Constructor" << endl; }
};
class B2{
public:
	B2(){cout << "B2:Constructor" << endl; }
	~B2(){cout << "B2:Constructor" << endl; }
};
class B3{
public:
	B3(){cout << "B3:Constructor" << endl; }
	~B3(){cout << "B3:Constructor" << endl; }
};

class A
{
	B1 b1;
	B2 b2;
	B3 b3;
pubilc:
	A():b3(),b2(),b1(){ cout<< "A:Constructor" << endl; }
	~A() { cout << "A:Destructor" << endl; }
};

void main()
{
	A a;
}

//Output
B1:Constructor
B2:Constructor
B3:Constructor
A:Constructor
B3:Destructor
B2:Destructor
B1:Destructor
常类型
  1. 常对象:
类名 const 对象名 / const 类名 对象名
  • 声明为常对象的同时必须被初始化,并从此不能改写对象的数据成员。
  • 常对象只能调用类的常成员函数或类的静态成员函数

const Sample a(10);

  1. 常对象成员:
    1. 常成员函数
      数据类型 函数名(参数表) const;
  • 常成员函数不更新对象的数据成员,也不能调用该类中没有用const修饰的成员函数
  • 如果将一个对象声明为常对象,则通过该常对象只能调用它的常成员函数,而不能调用其他成员函数
  • const关键字可以被用于参与对重载函数的区分:
    	void print();
    	void print() const; //正确的重载
    
    1. 常对象成员:
      只能通过初始化列表对该数据成员进行初始化
    class Sample
    {
    	const int n;
    public:
    	Sample(int i):n(i){}
    }
    

Chapter-2 类和对象(错题)


  • [例2.32]
    已知f1(int)是类A的公有成员函数,p是指向成员函数f1的指针,为其赋值时正确的是:p=A::f1

  • [例2.35]

    class Test
    {
    public:
    	Test(){}
    	~Test(){cout << '#'; }
    };
    
    void main(){ 
    	Test temp[2], *pTemp[2];   // 执行程序输出:2个'#'号
    }
    

    Test *pTemp[2]只建立了两个Test对象指针的数组,并没有创建Test对象,不会调用构造函数和析构函数

  • [例2.37]

    class point
    {
    public:
    	point() { cout<<"C"; }
    	~point() { cout<<"D"; }
    };
    
    void main()
    {
    	point *ptr;
    	point A,B;
    	point *ptr_point = new point[3];
    	//output: CCCCCDD
    }
    

    没有delete ptr_point 因此没有调用对应的析构函数

  • [例2.55]

class B
{
	int x,y;
public:
	B() {x=y=0; cout << "Constructor1" << endl; }
	B(int i) {x=i;y=0; cout << "Constructor2" << endl; }
	B(int i, int j) {x=i;y=j; cout << "Constructor3" << endl; }
	~B(){ cout<<"Destructor" << endl; }
	void print()
	{ cout << "x=" << ",y=" << y << endl;}
}

void main()
{
	B *ptr;
	ptr = new B[3];
	ptr[0] = B();
	ptr[1] = B(5);
	ptr[2] = B(2,3);
	for(int i=0;i<3;i++)
		ptr[i].print();
	delete[] ptr;
}

//output:
Constructor1
Constructor1
Constructor1
Constructor1
Destructor
Constructor2
Destructor
Constructor3
Destructor
x=0,y=0
x=5,y=0
x=2,y=3
Destructor
Destructor
Destructor

Chapter-3 引用


引用的概念
  • 引用不是变量,引用必须初始化

  • 引用不是值,不占存储空间,引用只有申明,没有定义

  • 引用只在声明时带有&,以后就像普通变量一样使用,不能再带&

  • 指针变量也可引用

    void main()
    {
    	int n=10, *pn=&n, *&rn=pn;
    	(*pn)++;   //n=11
    	(*rn)++;   //n=12
    }
    
  • void的引用是不允许的

  • 不能建立引用的数组

  • 没有引用的引用

  • 没有空引用

引用作为函数参数
class Sample{...};
void fun(Sample s1,Sample &s2)
{
	s1.setxy(12,18);   //不能对目标对象操作
 	s2.setxy(23,15);   //能对目标对象操作
}
常引用

常引用往往用作函数的形参,这样该函数中并不能更新该参数所引用的对象,从而保护实参不被修改

	int x = 2;
	const int &n = x;
	
	n++; //错误,不能通过常引用更新对象,但执行x++是正确的
引用和指针的区别

两者不同点:

  • 指针是个实体,而引用仅是个别名;引用不是变量,引用必须初始化,指针是变量,可以不用初始化。
  • 指针是变量,可以不初始化。
  • 引用不是值,不占存储空间,而指针变量会占用存储空间

Chapter-4 友元函数


友元函数概念
  • 友元函数是一种能够访问类中的私有成员的非成员函数,提高了程序的运行效率,破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员
  • 友元函数可以是多个类的友元friend double dist(Line l, Point p);
  • 友元关系不能被继承
  • 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元
友元类
class A
{
	...
public:
	friend class B;
	...
}

当一个类作为另一个类的友元时,意味着这个类的所有成员函数(B)都是另一个类的友元函数(A),可以调用另一个类的私有变量(A)

Chapter-5 运算符重载


重载为类的成员函数 类名 operator 运算符(参数表)
class Point
{
	int x,y;
public:
	Point(){}
	Point(int i, int j) {}
	Point operator+(Point &p){ return Point(x+p.x,y+p.y); }
};
重载为类的友元函数
class Point
{
	int x,y;
public:
	Point(){}
	Point(int i, int j) {}
	friend Point operator+(Point &p1, Point &p2)
	{ return Point(p1.x+p2.x,p1.y+p2.y); }
};
  1. 一般情况下,单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数(=、()、[]、-> 不能重载为类的友元函数
  2. 当需要重载运算符具有可交换性时,选择重载为友元函数
其他运算符重载
  • 一元自加/自减运算符重载
    class Number
    {
    	int x;
    public:
    	Number(){ x=0; }
    	Number(int i){ x=i; }
    	void disp(){ cout<< "x=" << x << endl;}
    	void operator++() {x++;}    //前置运算符
    	void operator++(int) {x+=2;}//后置运算符
    }
    
    void main()
    {
    	Number obj(5);
    	obj.disp();
    	++obj;
    	obj.disp();
    	obj++;
    	obj.disp();
    }
    
    //output:
    x=5
    x=6
    x=8
    
  • 算术赋值运算符重载
    class Vector
    {
    	int x,y;
    public:
    	void operator+=(Vector D) { x+=D.x; y+=D.y; }
    	void operator-=(Vector D) { x-=D.x; y-=D.y; }
    }
    
  • 关系运算符重载
    class Rect
    {
    	int length,width;
    public:
    	friend int operator>(Rect r1, Rect r2)
    	{ return r1.length*r1.width > r2.length*r2.width?1:0 }
    }
    

Chapter-6 继承与派生


Alt text

单继承派生类的构造函数调用顺序
1. 基类的构造函数
2. 子对象类的构造函数(如果有的话)
3. 派生类构造函数

当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造函数的途径。基类中有默认的构造函数或者根本没有定义构造函数时,派生类不必负责调用积累的构造函数

  • 基类指针引用一个派生类的对象。这种引用方式是安全的,但这种方法只能引用基类成员
  • 派生类指针引用基类的对象,这种引用方式错误
多继承派生类的构造函数调用顺序
1. 调用基类的构造函数,调用次序按照它们被继承时声明的次序(从左向右)
2. 子对象类的构造函数,调用次序按照它们在类中声明的次序(与参数表顺序无关)
3. 调用派生类构造函数
#include<iostream>
using namespace std;

class Base1
{
    public:
        Base1(int x)
            {cout<<"基类1构造函数"<<"X1= "<<x<<endl;}
        ~Base1()
            {cout<<"基类1析构函数"<<endl; }
};
class Base2
{
    public:
        Base2(int x)
            {cout<<"基类2构造函数"<<"X2= "<<x<<endl;}
        ~Base2()
            {cout<<"基类2析构函数"<<endl; }
};
class Base3
{
    public:
        Base3()
            {cout<<"基类3构造函数"<<endl;}
        ~Base3()
            {cout<<"基类3析构函数"<<endl; }
};

class A:public Base2,public Base3,public Base1
{
    public:
        A(int a,int b,int c,int d)
            :Base1(a),Base2(b),m1(c),m3(),m2(d)
            //此处如果基类构造函数没有参数,则可省略
            //基类和子函数的陈列,且顺序随意 
            //or:Base1(a),Base2(b),m1(c),m2(d)
        {
            cout<<"派生类构造函数"<<endl;
        }
        ~A()
        {
            cout<<"派生类析构函数"<<endl;
        }
    private:
    Base1 m1;
    Base2 m2;
    Base3 m3; 
};

int main()
{
    A obj(1,2,3,4);
    return 0;
}

//Output
基类2构造函数X2=2
基类3构造函数			//若基类无带参构造函数可以省略,因此参数表中无基类3
基类1构造函数X1=1
基类1构造函数X1=3		//我们需要按照他们在类中声明的顺序来分别构造基类1、基类2、基类3
基类2构造函数X2=4
基类3构造函数
派生类构造函数
派生类析构函数
基类3析构函数
基类2析构函数
基类1析构函数
基类1析构函数
基类3析构函数
基类2析构函数
虚基类
  • 虚基类的构造函数在非虚基类之前调用
  • 若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的顺序调用
  • 若虚基类由非虚基类派生而来,则仍然遵循先调用基类构造函数,再调用派生类中构造函数的执行顺序
class base1
{
public:
	base1() { cout << "class base1" << endl; }
};

class base2
{
public:
	base2() {cout << "class base2" << endl; }
}

class level1:public base2, virtual public base1
{
public:
	level1() {cout <<"class level1" << endl;}
}

class level2:public base2, virtual public base1
{
public:
	level2() {cout <<"class level1" << endl;}
}

class toplevel:public level1, virtual public level2
{
public:
	toplevel() {cout <<"class toplevel" << endl;}
}

void main() { toplevel obj; }

//output
class base1
class base2
class level2
class base2
class level1
class toplevel1

toplevel1类中,level2为虚基类,因此,尽管level1在level2之前说明,但还是level2的执行在先。
level2中,base1类为虚基类,先执行。同一个虚基类只需要初始化一次,所以level1时不需要再初始化base1
Alt text

Chapter-6 继承与派生(错题)


  • [例7.31]
class base
{
public:
	void who() { cout<<"base class"<<endl; }
}
class derive1:public base
{
public:
	void who() { cout<<"derive1 class"<<endl; }
}
class derive2:public base
{
public:
	void who() { cout<<"derive2 class"<<endl; }
}

void main()
{
	base obj1,*p;
	derive1 obj2;
	derivel obj3;
	p = &obj1;
	p->who();
	p = &obj2;
	p->who();
	p = &obj3;
	p->who();
	obj2.who();
	obj3.who();
}

//output:
base class
base class
base class
derive1 class
derive2 class

指针引起的普通成员函数的调用仅仅与指针类型有关,和此刻指针正指向的对象无关

Chapter-7 多态性和虚函数


静态联编&动态联编
  • 静态联编:编译时就解决了程序中的操作调用与执行该代码间的关系
  • 动态联编:只有在程序执行时才能确定将要调用的函数(虚函数支持下实现)
    • 动态联编实现条件:
        1. 类之间为基类与派生类关系
        1. 要有虚函数
        1. 调用虚函数操作的是指向对象的指针或者对象引用,或者由成员函数调用虚函数
虚函数
  • 一个函数被声明为虚函数,即使重新定义类时没有声明虚函数,那么它从这点之后的继承层次结构中都是虚函数(若派生类声明为虚函数,基类没有声明为虚函数,应该以基类为准)
  • 派生类的虚函数与基类中对应的虚函数的参数不同时,派生类的虚函数将丢失虚特性,变为重载函数
纯虚函数 & 抽象类

纯虚函数:基类中不能对虚函数给出有意义的定义
抽象类:带有纯虚函数的类称为抽象类,唯一用途是为其他类提供合适的基类

class Class_Name
{
	virtual 类型 函数名(参数名) = 0;
}
虚析构函数
class A
{
	virtual ~A() { cout<<"调用A::~A()" << endl; };
}

class B:public A
{
	virtual ~B() { cout<<"调用B::~B()" << endl; };
}

void main()
{
	A *a = new B();
}

//Output
调用B::~B()
调用A::~A()

//如果没有虚析构函数Output
调用A::~A()

Chapter-8 异常处理


异常基础概念

catch处理程序的出现顺序很重要,在一个try块中,异常处理程序是按照它出现的顺序被检查的。只要找到一个匹配的异常类型,后面的异常处理都将被忽略

void f(int code)
{
	try
	{
		if(code==0) throw code;     //引发int类型的yi'chang
		if(code==1) throw 'x';      //引发字符型异常
		if(code==2) throw 12.345;   //引发double类型异常
	}
	catch(int n)
	{ cout << "捕获整数类型: "<< n << endl;}
	catch(...)                      //可以捕获任何异常
 	{ cout << "默认捕获"<< endl;}
	return;
}
void main()
{
	f(0);
	f(1);
	f(2);	
}

//Output
捕获整数类型: 0
默认捕获
默认捕获
posted @ 2019-10-28 11:02  slrn  阅读(907)  评论(0编辑  收藏  举报