C/C++与逻辑__笔记

C/C++

1.有虚函数的类有个virtual table(虚函数表),里面包含了类的所有虚函数,存放的是每个虚函数的函数入口地址,类中有个virtual table pointers,通常称为vptr,指向这个virtual table。虚函数表不占用类对象的存储空间,但是虚函数表指针占用类对象的存储空间。
虚函数存放在代码段,虚函数表在Linux/Unix中存放在可执行文件的只读数据段中(rodata),微软的编译器将虚函数表存放在常量段。
如果类的对象的内存是动态申请的,则该对象的所有成员均在堆区,相应的,该对象对应的vptr也在堆区。

查找具体的虚函数的地址:
在虚函数表中,虚函数按照声明的顺序被依次存放。虚函数表中存放的是函数指针。
找到虚函数表之后,根据虚函数的声明顺序便可找到对应的函数地址。

2.<> 条款34
纯虚函数也可以有定义,可以提供给派生类的非纯虚函数(派生类继承的虚函数仍然是虚函数)一个缺省的实现方式。(link)

B* b = new D1;
b->B::f(); //不是b- >f();

派生类可以不重写基类的纯虚函数,但是此时派生类不能实例化,不能创建对象。

3.虚函数可以为私有函数。
若基类的虚函数属性为public,派生类重写的虚函数属性为private,则通过基类指针可以调用派生类的虚函数,但是派生类指针不能调用派生类的虚函数。

#includeclass Derived;class Base {
public:virtual void fun() { std::cout << "Base Fun"; }
};class Derived : public Base {
private:void fun() { std::cout << "Derived Fun"; }
};int main()
{Base* ptr = new Derived;ptr->fun();return 0;
}

若基类的虚函数属性为private,派生类重写的虚函数属性为public,则通过基类指针不可以调用派生类的虚函数,但是若将main()函数声明为友元函数,则可以进行上述调用。
友元函数不受访问权限的控制,可以访问所有成员。若要限制友元函数权限,可以在类中定义一个子类,将函数声明为子类的友元函数。

#includeclass Derived;class Base {
private:virtual void fun() { std::cout << "Base Fun"; }friend int main();
};class Derived : public Base {
public:void fun() { std::cout << "Derived Fun"; }
};int main()
{Base* ptr = new Derived;ptr->fun();return 0;
}

4.静态函数不可以是虚函数(虚函数的调用过程)(link)。
静态成员函数不能声明为const函数。

#include
class Test
{public:// 编译错误: static成员函数不能为conststatic void fun() const { }// 如果声明为下面这样,是可以的。const static void fun() {}或类似于const static int fun() { return 0; }
};

5.虚函数不一定会表现出多态性,但虚函数表现多态性的时候不能被内联。例如实际对象调用的虚函数可以被内联展开,而基类指针或引用调用的虚函数会表现出多态性,因此不能被内联。

6.建立在整个类中都恒定的常量,对于每个对象来说,该常量是相同的。可以使用枚举常量或const static常量。
枚举常量不会占用对象的存储空间,在编译期间,枚举变量被全部求值。

7.虚继承:为了解决菱形继承中的命名冲突问题。
C++ 规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。
虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。

D::D(int a, int b, int c, int d): B(90, b), C(100, c), A(a), m_d(d){ }

虽然将 A() 放在了最后,但是编译器仍然会先调用 A(),然后再调用 B()、C(),因为 A() 是虚基类的构造函数,比其他构造函数优先级高。如果没有使用虚继承的话,那么编译器将按照出现的顺序依次调用 B()、C()、A() (即依次调用各个构造函数)。

8.析构函数不能、也不应该抛出异常。构造函数可以抛出异常。
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏等问题。

2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,可能会造成程序崩溃。

那么当无法保证在析构函数中不发生异常时, 该怎么办?

把异常完全封装在析构函数内部,决不让异常抛出函数之外。这是一种非常简单,也非常有效的方法。

~ClassName(){try{do_something();}catch(){  //这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。}
}

当构造函数、析构函数为公共成员函数时,可以被显式调用。
构造函数与析构函数没有返回值,没有返回类型。

A a = A();
a.~A();

9.const对象只能调用const成员函数、不能调用非const成员函数;
非const对象可以调用const成员函数。(link)

10.模板成员函数不可以是虚函数。(link)
模板类可以有虚函数。

11.C++类内可以定义引用数据成员.。
但是必须在构造函数的参数初始化列表中进行初始化,不能在构造函数的函数体内进行初始化。
因为初始化列表是初始化,而函数体内是赋值操作,对于普通的数据类型两种操作只有资源消耗的区别。但引用和const常量都是不能被赋值的,它们在类内只能在构造函数的参数初始化列表中被初始化。

12.初始化和赋值
初始化对象,就是初始化对象的数据成员,以及虚函数表指针。
赋值时,仅是对数据成员进行赋值。

赋值是在两个已经存在的对象间进行的;
初始化是要创建一个新的对象。
编译器会区别这两种情况,赋值的时候调用重载的赋值运算符,初始化的时候调用构造函数。如果类中没有构造函数,则编译器会提供一个默认的。默认的拷贝构造函数只是简单地复制类中的每个成员。
对于基本数据类型,二者差异不大。但是对于自定义类型,二者差异较大。

1)编译器首先调用A的默认构造函数构造a,而后调用A的赋值运算符函数将 t 的数据成员的值赋值给 a.
2)编译器直接调用拷贝构造函数,使用 t 的值来初始化 a.

class A
{
public:A() {cout << "Construct A" << endl ;} //默认构造函数A(int p):a(p){}//非默认构造函数A(const A& t) {cout << "Copy constructor." << endl ;this->a = t.a ;}A& operator = (const A& t) {cout << "assignment." << endl ;this->a = t.a ;return *this;}
private:int a ;
};class B
{
public:A ab;B(A& t) {ab=t;}  1)B(A& t): ab(t) {}  2)
};
int main()
{A p;B b(p);return 0;
}

13.constexpr
给编译器足够的信心在编译期去做被constexpr修饰的表达式的优化,请大胆地将我看成编译时就能得出常量值的表达式去优化我。
然而编译器并不会100%相信程序员,当其检测到func的参数是一个常量字面值的时候,编译器才会去对其做优化,否则,依然会将计算任务留给运行时。
override、final关键字
override:说明当前函数是重写的基类的虚函数。
final:说明当前虚函数不能被子类重写或当前类不能被继承。

当类的构造函数析构函数为私有函数时,该类也不能被继承。因为在构造或析构派生类对象时,要调用基类的构造函数或析构函数。

14.空类默认提供缺省构造函数、拷贝构造函数、析构函数、赋值运算符。

15.对象不存在,且没用别的对象来初始化,就是调用了构造函数;
对象不存在,且用别的对象来初始化,就是拷贝构造函数(编译器默认提供的是浅拷贝构造函数);
对象存在,用别的对象来给它赋值,就是赋值函数。

初始化对象,就是初始化对象的数据成员,以及虚函数表指针。
赋值时,仅是对数据成员进行赋值。

16.如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数,而不用进行函数定义。

17.对象的什么函数不能被声明为虚函数(构造函数,内联函数inline,静态函数)。

18.对于抽象基类,即使其纯虚函数有定义,此基类仍然是抽象基类,不能实例化。
但是可以声明抽象基类的指针、引用,并指向派生类的对象。

class A
{
public:virtual void fun()=0 { cout << "non_const\n"; }
};
class B :public A
{void fun()  { cout << "const\n"; }
};
int main()
{B b;A* a = &b;A& c=b;return 0;
}

19.构造函数和析构函数可以是私有或保护成员。
1)对于一个类,如果要求当前的程序中只能存在一个类的实例,则可以把构造函数声明为非公有成员。
2)如果要求一个类的对象只能在堆上生成,不能在栈上生成,则可以把析构函数声明为非公有成员。

class cal
{~cal() {};//无论类对象是栈对象还是堆对象,析构函数中都不能删除 this 指针
public:cal() {};void destroy(){delete this;//仅当类对象是堆对象时,才可以在成员函数中删除 this 指针}
};cal c;//编译出错,析构函数不可访问
cal* ca = new cal;

20.在析构函数中 delete this; 会造成无限递归;
对于栈对象,在其他成员函数中 delete this; 会出现指针错误(link)。
当使用delete的时候:
第一步,针对此内存会有一个(或更多)析构函数被调用,
第二步才会释放该内存。

21.在类外访问类的私有成员

22.类中数据成员分配内存的时机不同于其他地方的数据变量。
静态成员变量必须在类外赋值(不能称为在类外定义),在类内只能声明静态变量。因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。

但是静态常量可以在类内初始化,且必须在类内声明的时候初始化。

若是普通常量,则可以先声明,而后通过构造函数的初始化列表进行初始化。

类/结构体仅是一种声明,如果不实例化,是不会分配数据内存的(静态成员除外,因为静态变量是在编译期分配内存)。

如果类不实例化,则不占用数据内存,可能会占用代码段的内存(会默认添加四个成员函数,仅在需要的时候才会生成这四个函数)。
即使类中非静态成员变量被初始化,只要类不实例化,那么就不会为这个成员变量分配内存。因为类的非静态成员变量是与类的对象绑定的,存储在为类的实例所分配的内存区域中。

对类名使用 sizeof 运算符,所得到的大小是一个类的实例所要占据的内存的大小,并不是指类本身占用了内存。

C++不存在静态类/静态结构体,但是编译可以通过,不过会出现warning.

 //warning C4091: “static ”: 没有声明变量时忽略“acv”的左侧static class acv  {int ca = 1;
};

23.对于类的普通成员函数,是不需要存储其函数指针的。因为普通成员函数是静态绑定的。
静态绑定,是指在c++源代码编译时,编译器在 p->Print(); 处翻译成直接调用Test类中Print()的汇编代码,也就是在编译期,编译器就确定了被调函数的相对地址。而所谓的动态绑定,是指源码在编译的时候,编译器不是翻译成直接调用Test类中Print()的汇编代码,而是翻译成一个查找虚表,得到函数的相对地址的过程。

只有类对象才能调用非静态类成员函数(即使函数没有参数)。原因是类的非静态成员函数都内含了一个指向类对象的指针(即this指针),因而只有类对象才能调用(对于不同的类对象,this指针的值不同)。

静态类型:对象在声明时的类型,在编译期被确定;
动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

虚函数的调用属于动态绑定:调用虚函数时,究竟调用子类还是基类的函数实现,取决于发出调用的对象的动态类型。

不要重新定义基类虚函数的缺省参数值,因为缺省参数值是静态绑定的,而虚函数是动态绑定的。
(《Effective C++》条款37 )

基类指针可以指向派生类对象,但是无法使用不存在于基类只存在于派生类的元素(所以我们需要虚函数和纯虚函数)。原因是这样的:在内存中,一个基类类型的指针可合法访问的是覆盖N个单位长度(即基类对象大小)的内存空间。当其指向派生类的时候,由于派生类对象在内存中的布局是:前N个是基类的元素,N之后的是派生类自定义的元素。于是基类的指针就可以访问到基类的元素,但是此时无法访问到派生类自定义(就是N之后)的元素(link)。
在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的。
static_cast 可用于上行转换,
dynamic_cast 用于下行转换。

Base* p=new Base;
Derived* pd=new Derived;
p=static_cast<Base*>(pd); (1)
pd=dynamic_cast<Derived*>(p); (2)

24.空类在声明时,编译器不会生成任何成员函数。只会隐式生成1个字节的占位符。空类如果被实例化,会生成四个默认的成员函数:构造函数、拷贝构造函数、析构函数、赋值运算符重载函数,即仅在需要的时候生成默认的四个成员函数。

25.派生类会继承基类的私有成员,但是禁止在派生类中直接访问私有成员。

26.构造派生类对象时,为何要先调用基类构造函数。
1)因为派生类无法直接访问基类私有成员,所以需要通过基类构造函数来初始化基类私有成员。
2)派生类可能会使用基类的成员变量,如果不对其正确初始化,大多数情况下得不到预期的结果。

27.什么情况下必须调用拷贝构造函数:
1)一个对象以值传递的方式传入函数体;
2)一个对象以值传递的方式从函数返回;
3)一个对象需要通过另外一个对象进行初始化.

28.A* p=new B();
A是B的基类。
通过p调用非虚函数,则所调用的是基类A的成员函数。
通过p不能调用子类B的非虚成员函数。
若要观察栈对象的析构过程,可以将其放在一个局部语句块中。

class A
{
public:A(){std::cout << "A cons.\n";fun();}virtual void fun() { std::cout << "A_Fun()\n"; }~A(){std::cout << "A decons.\n";defun();}virtual void defun() { std::cout << "A_Defun()\n"; }
};
class B :public A
{
public:B(){std::cout << "B cons.\n";fun();}virtual void fun() { std::cout << "B_Fun()\n"; }~B(){std::cout << "B decons.\n";defun();}virtual void defun() { std::cout << "B_Defun()\n"; }
};
int main()
{if (true){A* p = new B();delete p;//B b;}return 0;
}

在这里插入图片描述
如果A的析构函数为虚函数,则
在这里插入图片描述
不论A的析构函数是否是虚函数,对于B b;或者B* b=new B();
在这里插入图片描述
即只要子类的析构函数被调用,则无论基类的析构函数是否是虚函数,基类的析构函数均会被调用。

对于多继承,例如A、B是C的基类,则对于C c; 或者 C* c=new C();,无论A、B的析构函数是否是虚函数,子类的析构函数都会被调用,且最先被调用。

而对于A* a=new C();
若A的析构函数是虚函数,且继承顺序是class C :public A, public B,则C的析构函数会被调用,那么基类的析构函数必定会被调用,所以无论B的析构函数是否是虚函数,B的析构函数都会被调用。调用顺序是C、B、A。
如果A的析构函数不是虚函数,且继承顺序是class C :public A, public B,那么程序仅调用A的析构函数。

但如果继承顺序是class C :public B, public A,则程序在调用A的析构函数之后,会崩溃,除非A、B的析构函数均是虚函数。
如果继承顺序是class C :public A, public B,则无论A、B的析构函数是不是虚函数,程序都会正常运行,只是执行结果不同。

构造时按照继承的顺序来构造,例如class C :public A, public B,则构造顺序为A、B、C,析构顺序为C、B、A。

C* pc = new C();
std::cout << pc << std::endl;
A* pa = (A*)pc;
std::cout << pc << " " << pa << std::endl;
B* pb = (B*)pc;
std::cout << pc << " " << pa << " " << pb << std::endl;

对于上述转换,如果继承顺序是class C :public A, public B ,则pc==pa, pc!=pb
如果继承顺序是class C :public B, public A,则pc==pb, pc!=pa
pc的类型始终都是C类型。

29.使用 dynamic_cast 时,要求类中必须存在至少一个虚函数。
dynamic_cast 转换是在运行时进行转换的,运行时转换就需要知道类对象的信息(继承关系等)。通过虚函数表在运行时获取这些信息。因为派生类会继承基类的虚函数表,所以通过这个虚函数表,我们就可以知道该类的父类,在转换的时候就可以用来判断对象有无继承关系。

class A 
{
public:virtual ~A() {}
};
class B: public A {}; void foo(A * pa)
{B * vb1 = dynamic_cast<B *>(pa);
}

30.this指针不占用对象内存,在调用成员函数时,this指针作为一个隐含的参数被传递给成员函数。
this指针指向类对象的首地址。

class C 
{
public:C(int d = 2) :a(d) {}void f(){cout << this << endl;}
};
int main
{C c(5);cout<<&c<<endl;   //二者值相同c.f();
}

31.若子类与父类有相同名称的成员变量,类的成员函数“就近”调用自己的成员变量。
若子类与父类有相同名称的成员变量,则子类的成员变量不会覆盖父类的相应的成员变量,二者都会存在于内存之中。
构造子类对象时,首先调用父类的构造函数,在给子类分配的内存区域中构造父类的数据成员以及虚函数表指针,而后再调用子类的构造函数构造子类自己的数据成员以及虚函数表指针。二者内存是依次排列的,不是交叉排列(link)。
在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的。
但是因为一个对象只有一个虚函数表指针,所以子类的虚函数表指针会把父类的虚函数表指针替换掉。
子类重写父类的虚函数,只是把子类的虚函数表中相应的函数指针替换成了子类重写的虚函数。并不意味着无法再调用父类的虚函数。
“就近原则”:当类要使用某个属性(例如成员函数、成员变量)时,若自身拥有相应属性,则“就近”使用自身的属性。若没有相应属性,则去父类中进行查询,若父类中存在相应属性,则使用父类的属性。否则,继续向上查找,直到查找不成功或找到相应属性。

class A
{
public:A(int d = 0) :a(d) {}int get(){return data();}virtual int data(){return a;}
private:int a;
};
class B:public A
{
public:B(int d = 1) :a(d) {}int data(){return a;}
private:int a;
};
class C :public B
{
public:C(int d = 2) :a(d) {}private:int a;
};int main()
{C c(5);cout << c.get() << " " << c.B::get() << " " << c.A::get() << endl;cout << c.data() << " " << c.B::data() << " " << c.A::data() << endl;return 0;
}
// 1 1 1 
// 1 1 0
class A 
{
public:virtual void fun() { cout << "A" << endl; }
};
class B :public A
{
public:void fun() { cout << "B" << endl; }
};
void show(A* a, A* b, A& c, A& d, A e, A f)
{a->fun();b->fun();c.fun();d.fun();e.fun();f.fun();
}
int main()
{A a;B b;show(&a, &b, a, b, a, b); // A B A B A Areturn 0;
}
class A 
{
public:A(int a = 1) :i(a) {}int PA() { return i; }
private:int i;
};
class B :public A
{
public:B() :A(2),a(10){}   1)B() :a(10){}        2)// PA()不能是私有成员函数,否则在派生类中无法访问void PB() { cout << A::PA() << " " << a << endl; }
private:int a;
};
int main()
{B b;b.PB(); // 1) => 2 10    2) => 1 10cout << endl;return 0;
}    

32.运算符重载

class A
{
public:A(int n) :a(n) {}A() {}void show() { cout << a << endl; }A operator+(const A& p){int t = this->a + p.a;return A(t);}A operator++()//重载运算符,不要改变运算符的语义。因此此处返回值不可为 void.{++this->a;return *this;}A operator++(int)//后缀自增运算符,int参数仅是为了区分前缀、后缀自增运算符{int t = this->a;++this->a;return A(t);}friend A operator-(const A& t,const A& p){int y = t.a - p.a;return A(y);}private:int a = 1;
};
int main()
{A k, t;A r = k + t;A p = k.operator+(t);r.show();k.show();t.show();p.show();cout << endl; // 2 1 1 2A we;++we;we.show();we++;we.show();A wr = we.operator++();wr.show();we.show();A wt = we.operator++(0);//后缀自增运算符,int参数仅是为了区分前缀、后缀自增运算符wt.show();we.show(); // 2 3 4 4 4 5cout<<endl;A m, n;A c = m - n;c.show();A g = operator-(c, n);//不能通过类对象调用 operator-() 函数g.show(); // 0 -1return 0;
}

指向类成员函数的函数指针、指向非成员函数的函数指针

class A
{
public:A(int n) :a(n) {}A() {}void show() { cout << a << endl; }
private:int a = 1;
};
void fun()
{cout << "fun\n";
}
int main()
{void (A:: * k)() = &A::show; // 必须加上取地址运算符,用以确定所要调用的函数的地址A a_;(a_.*k)(); // 1A d(2);A* b = &d;(b->*k)(); // 2void (*fu)() = fun; // 可以加上取地址运算符,也可以不添加取地址运算符//void (*fu)()= &fun;fu(); // funtypedef void (*fu_)(); // 此处是定义了一个类型,此类型为指向某类函数的指针fu_ p = &fun; // 可以加上取地址运算符,也可以不添加取地址运算符//fu_ p= fun;p();return 0;
}
class A
{
public:A() { cout << "A.\n"; }
};
int main()
{A a();// a是一个函数A b;cout << "123\n";A b[3];cout << "456\n";A* p[4];return 0;
}

33.当存在同名的非const成员函数与const成员函数时,非const对象会选择调用非const版本的成员函数。如果想使得代码能够复用,可以在非const成员函数中直接调用const成员函数。

class A
{
public:void fun() { cout << "non_const\n"; }void fun() const { cout << "const\n"; }
};
int main()
{A p;const A y;p.fun(); // non_consty.fun(); // constreturn 0;
}

逻辑

1.有一种玻璃杯质量确定但未知,需要检测。
有一栋100层的大楼,该种玻璃杯从某一层楼扔下,刚好会碎。
现给你两个杯子,问怎样检测出这个杯子的质量,即找到在哪一层楼刚好会碎?(知乎)

2.有100个苹果,两个人拿,每次都能拿1-5个。问怎么样才能保证自己拿到最后一个苹果?(link)

3.过河问题

4.用天平从n个小球中找出其中唯一一个较轻的,一次可以称多个,最少称几次?(link)
当只有3个小球时,称一次就可以解决问题。

5.25匹马,5个赛道,最少需要多少次比赛选出最快的3匹或者5匹马?(link)

6.有一个只包含0和1的字符串,有至多K次机会,每次机会可以将串中的某个0改成1,求解该串中最长的连续1的长度。(link)
在这里插入图片描述
7.智力题收录、智力题收录、智力题收录
不均匀的绳,对折两次,同时从四个端点开始点燃绳子,燃烧完毕所需时间不一定是15分钟。
假设第一次对折之后的两段绳子,燃烧完毕需要的时间分别为 t1、t2,则上述情况所需的时间是
0.5 * max(t1,t2).

8.一个岛上有红黄蓝三种颜色的兔子,数量分别为a,b,c,任意两只不同颜色的兔子碰撞变成两只第三种颜色的兔子,a,b,c满足什么样的关系可以使得有可能经过足够长时间的碰撞,岛上最终都是一种颜色的兔子?(link)
要考虑各个等式成立的条件!

9.rand5() 生成 rand7()

10.有一枚均匀的硬币,计算连续投掷出N次正面的平均次数的期望值(link)。

11.不借助中间变量,实现swap(a,b).

12.圆内任意画两条弦,其不相交的概率.

13.判断某点在给定直线的左侧还是右侧:向量叉乘(link).
判断某点是否在给定的三角形的内部:向量叉乘、面积(link).

14.约瑟夫环(link)(link)
i = 1,2,3,…,n:
F(i,m) = ( F(i-1,m) + m ) % i,(初始编号从 0 到 n-1)
F(i,m) = (F(i-1,m) + m - 1) % i +1,(初始编号从 1 到 n)
从F(1,m)到F(n,m), m是固定的。

15.异或满足交换律、结合律
1)一个数组,仅一个元素出现2次,其余元素出现1次,元素是从1到n。
把元素值放到对应下标的位置上、哈希表统计、数组求和再与连续数字的总和作比较。
2)一个数组,仅一个元素出现1次,其余元素出现2次,元素是从1到n。
哈希表统计、异或(相同元素值异或结果为0)。

16.药水问题(扩展)


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部