阅读笔记
- 才发现CC为标准UNIX C++编译器,而cc为标准UNIX C编译器。g++为GNU C++编译器,而GNU C++编译器的MS-DOS版本名为gpp。comeau编译器最为严格、标准。都是先生成.o文件,再生成.out文件。
- 在C中,main函数括号为空表示对是否接受参数保持沉默,而在C++中为空与跟void一样。C++中main函数默认返回0。
- cout是可扩展的,允许自行开发新的数据类型。
- short至少16位;int至少和short一样长;long至少32位,且至少与int一样长。
- sizeof对类型名使用时,必须加上括号。
- C++使用前一(两)位来标识数字常量的基数。如果第一位是1~9,则基数为10;如果第一位为0,第二位为1~7,则基数为8;如果前两位为0x或0X,则基数为16。
- 数据后缀标识存储的类型,如2344L,默认情况下为int型,除非太大,int装不下。
- char在默认情况下既不是有符号,也不是没符号。在符号很重要的情况下,一定要特别声明。
- E表示法适合于非常大和非常小的浮点数。如:+3.45E+6指的是3.45与1000000相乘的结果,E后为负数代表除以10的乘方。
- cout.setf(ios_base::fixed, ios_base::floatfield);,这样可以显示浮点数多余的零。
- 赋值计算在类型转换时,较大的数转换为较小的数时,原来的值可能超过目标类型的取值范围,这种情况结果将是不可确定的。将浮点型转换为整形时,C++采取截取(丢弃小数部分),而不是四舍五入。
- true被转换为1,false被转换为0,这些转换称为整形提升(int类型计算速度更快)。
- 传统C语言总是将float提升为double,即使两个操作数都是float。
- 强制类型转换的格式为:(typeName)value/typeName(value),第一种来自C语言,第二种来自C++语言。
- 用引号括起的字符串隐式的包括了结尾的空字符。如“S”表示两个字符,而‘S’单表示一个字符,更糟糕的是,“S”表示的是一个内存的地址。
- strlen函数返回的是存储在数组中字符串的长度,而不是数组本身的长度。另外strlen不会计算空字符。
- 为了能够输入多个单词,可以使用cin.getline()函数和cin.get()函数,getline丢弃了换行符,存储时用空字符替换,而get保留在了输入序列中,所以在使用这种方法时,需要不带任何参数的cin.get()调用读取下一个字符,也可以连续调用,如cin.get(name, ArSize).get()。
string在C++中是作为类出现。string类具有自动调整大小的功能。输入字符串的方法为getline(cin, str)。
1
2
3
4
5for (int i = 0; i < SIZE; i++)
{
cout << i + 1 << ": ";
getline(cin, list[i]);
}C++允许在结构声明中省略关键字struct,结构体成员中也能进行赋值操作。
- 结构中的位字段:字段的类型应该为整形或枚举型,接下来是冒号,冒号后面是一个数字,它指定了使用的位数。通常用在低级编程中。
- 匿名共用体:将共用体用于结构体中,它们的地址相同,所以不需要中间标识符。
- 枚举、枚举量:默认情况,第一个枚举量为0。另外枚举变量只定义了赋值运算,没有定义算术运算。可自动转换为int型。如果只打算使用常量,而不创建枚举类型的变量,则可以省略枚举类型的名称。可以创建多个值相同的枚举量。可以将在取值范围内的任意整数赋给枚举量。
- 面向对象编程和传统的过程性编程的区别在于,OOP强调的是在运行阶段(而不是编译阶段)进行决策。动态联编/静态联编。
- 如果给cout提供一个地址,如果指针的类型为char ,则cout将显示字符串,如果要显示字符串的地址,应该使用int 。
- cout.setf(ios::boolalpha)函数调用设置了一个标记,该标记命令cout显示true和false,而不是1和0。
- C++规定,逗号表达式的值是第二部分的值。如cats = (17, 24)为24。
- C-风格字符串是通过结尾的空值字符定义的,而不是由其所在数组的长度定义的。这意味着两个字符串即使被存储在长度不同的数组中,也可能是相同的。
- cin将忽略空格和换行,如需读取,请使用cin.get(ch),其返回的是cin对象,在需要bool值的地方可以自动转换。相当于C规则的getchar(ch)
- ctrl + D/Z相当于EOF,可以使用cin.eof()/cin.fail()返回的bool值进行检测,输入错误和EOF都将导致cin返回false,可以用cin.clear()进行清除然后重新输入。
- 从存储空间的角度来讲,指针数组比char数组的数组更为经济。
- if(17 < age < 35)的含义为if((17 < age) < 35),所以一直为真。
- &&的优先级高于||。
iostream 提供了一个预先定义好的名为cout的ostream对象,而我们必须声明自己的ofstream对象。如:
1
2
3
4
5ofstream outFile;
outFile.open("fish.txt");
double wt = 125.9;
outFile << wt;cin >> total >> choices,如果输入4 2,那么total为4,choice为2。
- 在C++中,当且仅当用于函数头或函数原型中,int *arr 和int arr[]的含义才相同。
二维数组函数定义:
1
2
3
4
5
6
7
8int sum(int ar2[][4], int size)
{
int total = 0;
for (int r = 0; r < size; r++)
for(int c = 0; c < 4; c++)
total += ar2[r][c];
return total;
}当结构较小时,按值传递最合理。较大时使用按址传递。
- 递归案例:ruler.cpp
函数指针基础知识
函数指针声明。
<img src="/images/funstr.png"> 例如 void estimate(int lines, double (*pf) (int));
使用指针来调用函数,见例子。
1
2
3
4
5
6double pam(int);
double (*pf) (int);
pf = pam;
double x = pam(4);
double y = (*pf)(4);
double z = pf(4);
内联函数不能递归。与C语言的宏类似。只需在定义和声明前加上inline即可。
必须在声明引用时将其初始化。
注意
:在C++中,使用const,当形参的类型不对或不是左值时,将会自动创建临时变量,这样将不能修改作为参数传递的变量。结构引用:const sysop & use(sysop & sysopref);,请见例程/类对象,应避免返回函数终止时不再存在的内存单元引用。通过传递引用而不是整个数据对象,可以提高程序的运行速度。
注意在返回值加上const,意味着不能使用返回的引用直接修改它指向的引用。参数类型为ostream &的函数可以接受ostream对象或声明的ofstream对象作为参数,见文件类。设置类如下
指导原则:
- 显示具体化的原型和定义应以template<>打头,并通过名称来指出类型,这样可以只交换结构体中的一部分成员。不要使用Swap()模板来生成函数定义,而应使用独立的、专门的函数显式的为job类型生成函数定义,如
template <> void Swap<job>(job&, job&);
,其中Swap<job>
中的<job>
是可选的。<例子> - 编译器使用模板为特地类型生成函数定义时,得到的是模板实例,有隐式实例化和显式实例化。显式实例化如template void Swap
(int, int);,它与显式具体化不同,template后面没有<>。而隐式实例化指的是一般的模板调用。 警告
:试图在同一编程单元中使用同一类型的显式实例和显式具体化将出错。 - 隐式实例化、显式实例化和显式具体化统称为具体化,它们都使用具体类型的函数调用,而不是通用描述。
- 在重载解析中,哪一种执行的转换最少,将被优先得到执行。如果出现多个匹配或没有,都将出现错误。<例子>
变量的一些特性:存储持续性、作用域和链接性。寄存器变量也是auto变量,只是它是用寄存器而不是堆栈来处理变量。链接分为:外链接、内链接和无链接。包含字符串输入的static例子
- mutable可以指出,即使结构或类为const,其某个成员也可以被修改。
- 在C++看来,全局const定义就像使用了static说明符一样。如果链接为外部的,extern const int states = 50;
- 布局new操作符、常规new操作符。布局操作符如:p1 = new (placement) int;,或pd2 = new (buffer + N*sizeof(double)) double[N];。布局操作符不需delete。<例子>
- 名称空间:namespace,其链接性是外部的,可以将名称空间声明进行嵌套。
可以给名称空间创建别名:namespace mvft = my_very_favorite_things {…};
简化:namespace MEF = myth::elements::fire; using MEF::flame; - 接收一个参数的构造函数允许使用赋值句法来将对象初始化为一个值,也可以看作为强制类型转换,可以使用explicit 关闭隐式转换,但仍然可以进行显式转换:myCat = Stonewt(19.6);,当然19.6也可以是int型,会自动转换为double型。<例子>
如何将类类型转换为其它内置类型?使用特殊的C++操作符函数——转换函数。如:Stonewt::operator int() const;,其返回类型可以是被声明为目标类型的类成员函数:Star::Star double();。<例子> 注意一下几点:
- 转换函数必须是类方法。
- 转换函数不能指定返回类型,但也能返回所需的值。
转换函数不能有参数。
如果不想被隐式的转换,可以换成int Stonewt::Stone_to_Int();,然后作为方法调用即可。
- const成员函数:void Stock::show()const,不会修改对象的值,这里的const表示show(const Stock *this),this指向调用的对象。
对象数组初始化像普通的数组一样。要创建对象数组,则这个类必须有默认的构造函数,但只能有一个默认构造函数,用来设定特定的值,因为如下花括号中的构造函数只是创建临时对象。
1
2
3
4
5
6const int STKS = 10;
Stock stocks[STKS] = {
Stock("NanoSmart", 12.5, 20),
Stock(),
Stock("Monolithic Obelisks", 130, 3.25),
}类只是描述了对象的形式,并没有真正创建对象,因此,在被对象创建之前,并没有用于存储值的空间。可以在类声明中声明枚举为整形常量,并且不需要提供枚举名。也可以采用关键字static,如static const int LEN = 30,但这个不能存储double常量,如果不是const,则应在定义方法时进行初始化,并且应该加上类限定符。
- 操作符重载限制:重载后的操作符必须至少有一个操作数是用户定义的类型。不能违反操作符原来的句法。不能修改操作符优先级。不能定义新的操作符。
- 友元有三种:友元函数、友元类和友元成员函数。
- 有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。非成员函数可以解决的问题:A = operator(2.75, B);,因为这个时候的第一个操作数即调用者不为自身对象,只能使用非成员函数。另外还有一种方法是,将2.75强制转换为对象。
注意
:虽然友元函数是在类声明中声明的,但它不是成员函数,因此不能使用成员操作符调用。它不是成员函数,但与成员函数访问权限相同。不要在定义中使用关键字friend,除非定义也是原型。类声明可以控制哪些函数可以访问私有数据,类方法和友元只是表达类接口的两种不同机制。 打印出Time类trip,重载操作符<<
1
2
3
4void operator <<(ostream & os, const Time & t)
{
os << t.hours << " hours. " << t.minutes << " minutes";
}即可使用cout << trip
如果要实现拼接,如cout << “Trip Time: “ << trip << “ (Tuesday)\n”;
可返回os的引用,如下1
2
3
4
5ostream & operator <<(ostream & os, const Time & t)
{
os << t.hours << " hours. " << t.minutes << " minutes";
return os;
}由于类继承属性让ostream引用能够指向ostream对象和ofsream对象,所以还可以将其写入文件中。<例子>
- 加法操作符需要两个操作数。对于成员函数版本来说,一个操作数通过this指针隐式的传递,另一个操作数作为函数参数显式传递;对于友元函数来说,两个操作数都作为参数来传递。
- 隐式成员函数包括:默认构造函数、复制构造函数、赋值操作符、默认析构函数和地址操作符。
- 复制构造函数:新建一个对象并将其初始化为同类现有对象时,复制构造函数都将会调用。即每当程序生成对象副本时,编译器都将使用复制构造函数。如:函数按值传递对象时,因此应该按引用传递对象,这样可以节省调用构造函数的时间以及存储新对象的空间。
- 赋值操作符:将一个已有的对象赋给另一个对象时,将使用重载的赋值操作符,来实现成员的逐个复制。解决的办法也是进行深复制。<例子>需要注意几点:
- 由于目标对象可能引用了以前分配的对象,所以应使用delete []释放。
- 应该避免将对象赋给自己。
- 返回一个指向调用对象的引用。
- new对应于delete,delete []与new []初始化的指针和空指针都兼容。如:str = new char[1]比str = new char要好。如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带,因为只有一个析构函数。
- 返回对象将调用复制构造函数,而返回引用不会。返回对象将调用复制构造函数和析构函数,有时候会浪费内存和时间。如:返回类型必须是ostream &,不能使用ostream,因为ostream没有公用的复制构造函数。如果被返回的对象是被调用函数中的局部变量,则应按对象方式返回,通过调用构造函数生成,这样的例子如重载加减运算符。
- 将加法重载操作符的返回值设为const,这样force1 + force2 = net;这样的语句将称为非法语句。
- 使用布局new操作符为对象分配内存时,防止出现内存重叠,可以这样:pc1 = new (buffer) JustTesting; pc3 = new (buffer + sizeof(JustTesting)) JustTesting;,释放内存应该这样:delete [] buffer;,并显式的调用析构函数pc3->~JustTesting();pc1->~JustTesting();。<例子>
- 引用和const一样,只能在对象创建时进行初始化。对于简单数据成员,使用成员初始化列表和在函数体中进行赋值并没有什么区别,不过效率更高。成员初始化列表只能用于构造函数。数据成员列表被初始化的顺序与它们在类声明中的顺序相同,与初始化列表的排列顺序无关。这使得初始化内置类型就像初始化类对象一样。
为了防止对象未定义深度复制构造函数而造成程序崩溃,可以在私有部分定义空的复制构造函数和重载空的赋值操作符。
1
2
3
4
5
6
7class Queue
{
private:
Queue(const Queue & q) : qsize(0) {}
Queue & operator =(const Queue & q) { return *this; }
//...
};构造函数必须给新成员(如果有的话)和继承的成员提供数据。由于派生类不能直接访问基类的私有成员,所以,派生类构造函数必须使用基类构造函数。创建派生类对象时,程序首先创建基类对象,使用成员初始化列表。
除非要使用默认构造函数,否则应显式调用正确的基类构造函数。
如果没有使用动态内存分配,在派生类构造函数里使用基类的隐式复制构造函数是可以的。
释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。<例子>- 基类与派生类的关系:
- 派生类对象可以使用基类的方法,条件是方法不是私有的。
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象。
- 基类引用可以在不进行显示类型转换的情况下引用派生类对象。
C++有三种继承关系:公有继承、保护继承和私有继承。
- 公有继承是最常用的方式,它建立一种
is-a
关系,即派生类对象也是一个基类对象,可以对基类对象执行任何操作,也可以对派生类对象执行。注意:is-a关系通常是不可逆的,也就是说,水果不是香蕉。公有继承不能建立has-a
、is-like-a
、is-implemented-as-a
、uses-a
关系。 - 多态公有继承:在派生类中重新定义基类方法。使用虚方法
virtual
,该关键字只出现在方法原型中。
对于虚函数,程序将根据对象类型来确定使用哪个版本。对于两个对象中行为相同的方法,只在基类中声明。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。
方法在基类中声明为虚拟的后,它在派生类中将自动成为虚方法,一般也都在派生类中指出。为基类声明一个虚拟析构函数也是一种惯例,可以确保正确的析构函数被调用。一般先调用派生类的析构函数,再调用基类的析构函数。
友元不能是虚函数,只有成员才能是虚函数,但可以让友元函数使用虚拟成员函数。
可以使用数组来表示多种类型的对象,这就是多态性。 访问控制protected:关键字protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。protected与private的区别只有在基类派生的类中才能表现出来。派生类的成员可以直接访问基类的保护成员,但不能访问基类的私有成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class TheOnlyInstance
{
public:
static TheOnlyInstance * GetTheOnlyInstance();
protected:
TheOnlyInstance() {}
}
TheOnlyInstance* TheOnlyInstance::GetTheOnlyInstance()
{
static TheOnlyInstance objTheOnlyInstance;
return &objTheOnlyInstance;
}
int main()
{
TheOnlyInstance noCanDo; //not allowed
TheOnlyInstance * pTheOnlyInstance = TheOnlyInstance::GetTheOnlyInstance(); //以后调用,将返回同一个静态地址
}使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生类公有接口的一部分,但可以在派生类的成员函数中使用它们。这种不完全继承是has-a关系的一部分,其特性与包含相同。
包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员,这是两种方法的主要区别。
成员初始化列表使用std::string(str),而不是name(str),这里string是基类。这是包含和私有继承之间的第二个主要区别。
使用作用域解析操作符可以访问基类方法,但如果要使用基类对象本身,可以使用强制类型转换:1
2
3
4const string & Student::Name() const
{
return (const string &) *this;
}
用类名显示地限定函数名不适合于友元函数,这是因为友元不属于类。不过可以显式的转换为基类来调用正确的函数。另一方面,如果不使用类型转换,由于使用的多重继承,编译器将无法确定转换成哪个基类。<例子>
1
2
3
4
5ostream & operator(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &) stu << ":\n";
...
}私有继承所提供的特性比包含多,但会引发许多问题。
私有继承可以重新定义虚函数,但也只能在类中使用。- 保护继承是私有继承的变体。基类的公有成员和保护成员都将成为派生类的保护成员,与私有不同,第三代的派生类能使用保护成员。
<img src="/images/dif.png">
- 公有继承是最常用的方式,它建立一种
如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。
在C++中,动态联编与指针和引用调用的方法相关,从某中程度上说,这是由继承控制的。编译器对非虚函数采用静态编联。也就是说,当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将调用的版本确定下来。
派生类的虚函数的返回类型形参类型必须与基类函数匹配,否则会隐藏同名的基类方法。只有一个例外,当类的虚函数的返回类型是类本身的指针或引用时,这称为返回类型协变。
如果基类声明被重载了,则应在派生类中重新定义所有的基类版本,否则其他的版本都将被隐藏。
将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这时公有继承不需要进行显式类型转换,这种转换是可以传递的。
仅将那些预期将被重新定义的方法声明为虚拟的。构造函数不能是虚函数,析构函数应当是虚函数。- 编译器处理虚函数的方法:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这种数组称为虚函数表。
虚函数的实现原理:
虚函数的实现需要利用虚表(Virtual Table)和虚表指针(Virtual Table Pointer)。虚表是一个指针数组,它存储了类的虚函数的地址,虚表指针是一个指针,指向该对象所属的类的虚表首地址。在调用虚函数时,程序通过虚表指针找到虚表,再根据函数的索引(函数在虚表中的下标)找到对应的虚函数并执行。当一个类被继承时,派生类会继承父类的虚表地址,并在其中添加新的虚函数。如果派生类需要覆盖父类的虚函数,那么它会把新的虚函数地址写入虚表中对应的位置。这样就实现了运行时的多态性。
- 抽象基类:当类声明中包含纯虚函数时,则不能创建该类的对象。要真正的成为抽象基类,则至少应包含一个纯虚函数。原型中的=0使虚函数成为纯虚函数。C++允许纯虚函数有定义,也可以不定义。纯虚方法是定义派生类的通用接口。
如果基类派生类都使用动态内存分配,则必须为派生类定义显式析构函数、复制构造函数和赋值操作符,也就是说,必须使用相应的基类方法处理基类元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class hasDMA : public baseDMA
{
private:
char * style;
public:
...
};
hasDMA & hasDMA::operator =(const hasDMA & hs)
{
if (this == &hs)
return *this;
baseDMA::operator =(hs); // 重点!
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}派生类对象的友元函数可以通过基类的友元函数访问基类的成员,由于友元不是成员函数,友元函数不能继承,不能使用作用预解析符,所以可以相应类强制类型转换选择正确的函数。<例子>
1
2
3
4
5
6
7std::ostream & operator <<(std::ostream & os, const hasDMA & hs)
{
os << (const baseDMA &)hs; // 相应类强制类型转换
//也可以:os << dynamic_cast<const baseDMA &> (hs)
os << "Style: " << hs.style << std::endl;
return os;
}- 通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。
包含对象成员的类:使用公有继承时,类可以继承接口,可能还有实现。获得接口是is-a关系的组成部分,而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
对比私有继承:
对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。例如,string类将+操作符重载为将两个字符串连接起来;但从概念上说,将两个Student对象串接起来是没有意义的。
被包含对象的接口不是公有的,但可以在类方法中使用它。<例子>1
2
3
4
5
6
7double Student::Average() const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}私有辅助方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#=> 位于private
ostream & Student::arr_out(ostream & os) const
{
int i;
int lim = scores.size();
if (lim > 0)
{
for (i = 0; i < lim; i++)
{
os << scores[i] << " ";
if (i % 5 != 0)
os << endl;
}
}
else
os << " empty array ";
return os;
}使用using重新定义访问权限:
方法一是定义一个使用该基类方法的派生类方法。
1
2
3
4double Student::sum() const
{
return std::valarray<double>::sum();
}另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(将像名称空间那样)来指出派生类可以使用的特定的基类成员,即使使用的是私有派生。
注意
:using 声明只使用成员名——没有圆括号、函数特征标和返回类型。using声明只适合继承,而不适用于包含。1
2
3
4
5
6
7
8class Student : private std::string, private std::valarray<double>
{
...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
...
}
虚基类:虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。例如:通过在类声明中使用关键字virtual, 可以使Worker被用作Singer和Waiter的虚基类
1
2class Singer : virtual public Worker { ... };
class Waiter : public virtual Worker { ... };可以将SingingWaiter类定义为
1
class SingingWaiter : pulic Singer, public Waiter { ... };
友元类:
- 当一个类B成为了另外一个类A的“朋友”时,那么类A的私有和保护的数据成员就可以被类B访问。我们就把类B叫做类A的友元。
- 友元类可以通过自己的方法来访问把它当做朋友的那个类的所有成员。但是我们应该注意的是,我们把类B设置成了类A的友元类,但是这并不会是类A成为类B的友元。说白了就是:甲愿意把甲的秘密告诉乙,但是乙不见得愿意把乙自己的秘密告诉甲。
- 声明友元类的方法其实很简单,只要我们在类A的成员列表中写下语句:friend class B;这样一来,类B就被声明成了类A的友元。注意,类B虽然是类A的友元,但是两者之间不存在继承关系。这也就是说,友元类和原来那个类之间并没有什么继承关系,也不存在包含或者是被包含的关系。
拷贝构造函数和赋值运算符
在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。
拷贝构造函数必须以引用的方式传递参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
1 | class Person |
C++ std::string写时复制与深浅拷贝,默认是浅拷贝,写时进行深拷贝!如果要强制使用深拷贝,使用地址c_str()进行构造。1
2
3
4
5
6
7
8std::string a("test");
std::string b = a;
printf("%p: %s\n", a.c_str(), a.c_str());
printf("%p: %s\n", b.c_str(), b.c_str());
std::cout << "---after---" << std::endl;
b[0] = 'T';
printf("%p: %s\n", a.c_str(), a.c_str());
printf("%p: %s\n", b.c_str(), b.c_str());
1 | const int* c_PtrA = new int(10); |
- const int * 表示,指向常量int 类型的指针,即指向的这块内存的内容,不可以修改。(但指针本身可以修改)。
- int * 表示,指向非常量int类型的指针,指针这块内存的内容可修改,指针本身可修改。
- const int 已经限制此地址内容,不可修改。这时,却让 int 指针指向这块地址,而使用 int * 指针,表示此地址内容可修改。那么到底可不可以改?
- 接上,因此逻辑冲突,编译器报错。
1 | enum COLOR { |
C++中的函数隐藏机制,子类重载了父类的函数,父类的函数就被隐藏了,除非加using或基类限定符。
我们假设,没有隐藏机制,子类可以继承父类的其他所用同名的函数,那么会出现什么问题?
单继承是没问题的,多继承呢,如果派生类继承自Base1, Base2。而两个基类都用好几个func函数,那么派生类该使用哪一个呢?
引入函数隐藏机制,就解决了这个问题,如果使用Base1,就using Base1::func; 使用Base2中的函数,就using Base2::func;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Base {
public:
virtual void test(int a, int b)
{
std::cout << a << " " << b << std::endl;
}
};
class Derived : public Base {
public:
// using Base::test;
void test()
{
int a = 1;
int b = 4;
Base::test(a, b);
std::cout << "Derived" << std::endl;
}
};
malloc的时候发生异常,异常处理函数里面在malloc就会阻塞,如下程序,运行时按ctrl+c会复现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using namespace std;
void signal_handler(int signum);
void* test( void* args )
{
while(1)
{
printf("try to malloc in test, thread id = %u\r\n", pthread_self());
void *p = malloc(NUM);
}
return NULL;
}
int main()
{
signal(SIGINT, signal_handler);
pthread_t thread1;
pthread_create(&thread1, NULL, test, NULL);
for(;;)
{
printf("malloc in main function, thread id = %u\r\n", pthread_self());
void *p = malloc(NUM);
}
pthread_join(thread1, NULL);
return 0;
}
void signal_handler(int signum)
{
printf("receive SIGTERM, malloc again, thread id = %u\r\n", pthread_self());
void *p = malloc(NUM);
}
memset to struct 引起的 core,因为,memset(&,0,sizeof()) 会把 struct 结构体内的 所有复位 为0,内含的 string 对象 被毁坏了,在析构时 string对象的析构调用问题,对应 struct 内含 对象 最好不要用 memset 这类函数。
在 C++ 的多继承中,每个基类都会有一个对应的虚函数表(vtable)来存储其虚函数的地址。当派生类继承了多个基类时,其内存结构中将会有多份虚函数表。
菱形继承会导致类中出现重复的数据和函数,增加代码的复杂性和内存占用。为了避免这个问题,C++提供了虚继承(virtual inheritance)机制,通过这个机制来解决多重继承时的菱形继承问题。虚继承可以保证共同的基类在最终的派生类中只存在一份共享实例,有效地避免了菱形继承问题。
虚继承的内存布局情况如下:
- 如果一个类不是虚基类,则按照普通的继承方式,将基类的数据成员和成员函数复制到派生类中。
- 如果一个类是虚基类,则在派生类中只保留一个指向虚基类的指针v_ptr,不直接复制虚基类的成员数据和函数。这个指针是在虚表中的,虚表指针指向了指向虚基类的指针。
同名虚函数在两个或更多父类中出现时,需要在派生类中明确地重写虚函数并使用作用域解析运算符来指定要使用哪个父类中的实现。
1 | class Derived: public A, public B { |
结构体内存对齐
内存中的任何数据的存储地址必须是其本身数据类型大小的整数倍,否则会发生内存地址对齐错误(alignment glitch),这也是内存对齐最基本的原则。
举个例子:如果一个 int 类型的变量从内存地址 X 开始存储,那么地址 X 的值必须是 4 的倍数,否则就是不对齐的情况。
结构体对齐原则:
- 结构体每个成员变量的起始地址相对于结构体首地址的偏移量必须为该成员大小的整数倍。
- 结构体类型的总大小必须是结构体所有非位域成员大小的整数倍。换言之,结构体的大小必须是其成员中所占空间最大的成员大小的整数倍。
- 优化原则:结构体内的基本变量类型(比如 int,char 等)可以不对齐,但结构体内自定义类型的变量一定要对齐。因为自定义类型会在内存中开辟一段新的存储空间,不对齐很容易导致开发中易错难调的问题。
如何限制对象只能建立在堆上或者栈上
把构造、析构函数设为 protected 属性,再用子类来动态创建,类对象就无法建立在栈上了。只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。
define 和 const 区别
对于 define 来说, 宏定义实际上是在预编译阶段进行处理,没有类型,也就没有类型检查,仅仅做的是遇到宏定义进行字符串的展开,遇到多少次就展开多少次,而且这个简单的展开过程中,很容易出现边界效应,达不到预期的效果。因为 define 宏定义仅仅是展开,因此运行时系统并不为宏定义分配内存,但是从汇编 的角度来讲,define 却以立即数的方式保留了多份数据的拷贝。
对于 const 来说, const 是在编译期间进行处理的,const 有类型,也有类型检查,程序运行时系统会为 const 常量分配内存,而且从汇编的角度讲,const 常量在出现的地方保留的是真正数据的内存地址,只保留了一份数据的拷贝,省去了不必要的内存空间。而且,有时编译器不会为普通的 const 常量分配内存,而是直接将 const 常量添加到符号表中,省去了读取和写入内存的操作,效率更高。
C++ 中重载和重写,重定义的区别
- 重载,翻译自 overload,是指同一可访问区内被声明的几个具有不同参数列表的同名函数,依赖于 C++函数名字的修饰会将参数加在后面,可以是参数类型,个数,顺序的不同。根据参数列表决定调用哪个函数,重载不关心函数的返回类型。
- 重写,翻译自 override,派生类中重新定义父类中除了函数体外完全相同的虚函数,注意被重写的函数不能是 static 的,一定要是虚函数,且其他一定要完全相同。要注意,重写和被重写的函数是在不同的类当中的,重写函数的访问修饰符是可以不同的,尽管 virtual 中是 private 的,派生类中重写可以改为 public。
- 重定义(隐藏),派生类重新定义父类中相同名字的非 virtual 函数,参数列表
和返回类型都可以不同,即父类中除了定义成 virtual 且完全相同的同名函数才
不会被派生类中的同名函数所隐藏(重定义)。
什么情况下会调用拷贝构造函数(三种情况)
类的对象需要拷贝时,拷贝构造函数将会被调用,以下的情况都会调用拷贝构造函数:
- 一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象压入到栈空间中。
- 一个对象以值传递的方式从函数返回,需要执行拷贝构造函数创建一个临时对象作为返回值。
- 一个对象需要通过另外一个对象进行初始化。
构造函数为什么一般不定义为虚函数
虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用的,如果说构造函数是虚的,那么虚函数表指针则是不存在的,无法找到对应的虚函数表来调用虚函数,那么这个调用实际上也是违反了先实例化后调用的准则。
预处理,编译,汇编,链接程序的区别
一段高级语言代码经过四个阶段的处理形成可执行的目标二进制代码。
预处理器→编译器→汇编器→链接器:最难理解的是编译与汇编的区别。
这里采用《深入理解计算机系统》的说法。
- 预处理阶段: 写好的高级语言的程序文本比如 hello.c,预处理器根据 #开头的命令,修改原始的程序,如#include 将把系统中的头文件插入到程序文本中,通常是以 .i 结尾的文件。
- 编译阶段: 编译器将 hello.i 文件翻译成文本文件 hello.s,这个是汇编语言程序。高级语言是源程序。所以注意概念之间的区别。汇编语言程序是干嘛的?每条语句都以标准的文本格式确切描述一条低级机器语言指令。不同的高级语言翻译的汇编语言相同。
- 汇编阶段: 汇编器将 hello.s 翻译成机器语言指令。把这些指令打包成可重定位目标程序,即 .o文件。hello.o是一个二进制文件,它的字节码是机器语言指令,不再是字符。前面两个阶段都还有字符。
- 链接阶段: 比如 hello 程序调用 printf 程序,它是每个 C 编译器都会提供的标准库 C 的函数。这个函数存在于一个名叫 printf.o 的单独编译好的目标文件中,这个文件将以某种方式合并到 hello.o 中。链接器就负责这种合并。得到的是可执行目标文件。在程序编译之后,生成的目标文件中包含有待解析的未知符号。在链接过程中,链接器会根据符号表中的信息找到这些未知符号的地址,并将其添加到程序的符号表之中。链接器还会进行重定位操作,将程序中的绝对地址转换为相对地址。这个过程中,链接器会生成对全局变量、函数等符号的引用关系,并将这些符号的引用关系绑定到正确的地址上。完成链接后,生成可执行二进制文件并将其装载到内存中,程序便可以运行了。
构造函数的执行顺序?析构函数的执行顺序?
构造函数顺序
- 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
- 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
- 派生类构造函数。
析构函数顺序 - 调用派生类的析构函数;
- 调用成员类对象的析构函数;
- 调用基类的析构函数。
简单说一下函数指针
从定义和用途两方面来说一下自己的理解:
- 首先是定义:函数指针是指向函数的指针变量。函数指针本身首先是一个指针变量,该指针变量指向一个具体的函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。
- 在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。
- 其次是用途:调用函数和做函数的参数,比如回调函数。
示例:1
2
3
4char * fun(char * p) {…} // 函数fun
char * (*pf)(char * p); // 函数指针pf
pf = fun; // 函数指针pf指向函数fun
pf(p); // 通过函数指针pf调用函数fun
何时需要成员初始化列表?过程是什么?
- 当初始化一个引用成员变量时;
- 初始化一个 const 成员变量时;
- 当调用一个基类的构造函数,而构造函数拥有一组参数时;
- 当调用一个成员类的构造函数,而他拥有一组参数;
list中的项目顺序是由类中的成员声明顺序决定的,不是初始化列表中的排列顺序决定的。
简述C++ 中 const 关键词
- const 修饰基本类型数据类型:基本数据类型,修饰符 const 可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值即可。
- const 修饰指针变量和引用变量:如果 const 位于小星星的左侧,则 const 就是用来修饰指针所指向的变量,即指针指向为常量;如果 const 位于小星星的右侧,则 const 就是修饰指针本身,即指针本身是常量。
- const 应用到函数中:作为参数的 const 修饰符:调用函数的时候,用相应的变量初始化 const 常量,则在函数体中,按照 const 所修饰的部分进行常量化,保护了原对象的属性。 [注意]:参数 const 通常用于参数为指针或引用的情况; 作为函数返回值的 const 修饰符:声明了返回值后,const 按照”修饰原则”进行修饰,起到相应的保护作用。
- const 在类中的用法:const 成员变量,只在某个对象生命周期内是常量,而对于整个类而言是可以改变的。因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进行。const 成员函数:const 成员函数的主要目的是防止成员函数修改对象的内容。要注意,const 关键字和 static 关键字对于成员函数来说是不能同时使用的,因为 static 关键字修饰静态成员函数不含有 this 指针,即不能实例化,const 成员函数又必须具体到某一个函数。
- const 修饰类对象,定义常量对象:常量对象只能调用常量函数,别的成员函数都不能调用。
补充:const 成员函数中如果实在想修改某个变量,可以使用 mutable 进行修饰。成员变量中如果想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现或者 static const。
C ++ 中的 const类成员函数(用法和意义)
- 常量对象可以调用类中的 const 成员函数,但不能调用非 const 成员函数; (原因:对象调用成员函数时,在形参列表的最前面加一个形参 this,但这是隐式的。this 指针是默认指向调用函数的当前对象的,所以,很自然,this 是一个常量指针 test const,因为不可以修改 this 指针代表的地址。但当成员函数的参数列表(即小括号)后加了 const 关键字(void print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test const,即不可以通过 this 指针来改变指向对象的值。
- 非常量对象可以调用类中的 const 成员函数,也可以调用非 const 成员函数。
多态的实现
- 多态其实一般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数生成符号表时的不同规则,重载只是一种语言特性,与多态无关,与面向对象也无关,但这又是 C++中增加的新规则,所以也算属于 C++,所以如果非要说重载算是多态的一种,那就可以说:多态可以分为静态多态和动态多态。
- 静态多态其实就是重载,因为静态多态是指在编译时期就决定了调用哪个函数,根据参数列表来决定;
- 动态多态是指通过子类重写父类的虚函数来实现的,因为是在运行期间决定调用的函数,所以称为动态多态,
- 一般情况下我们不区分这两个时所说的多态就是指动态多态。
- 动态多态的实现与虚函数表,虚函数指针相关。
- 扩展: 子类是否要重写父类的虚函数?子类继承父类时, 父类的纯虚函数必须重写,否则子类也是一个虚类不可实例化。 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
new / delete ,malloc / free 区别
- 都可以用来在堆上分配和回收空间。new /delete 是操作符,malloc/free 是库函数。
- 执行 new 实际上执行两个过程:1.分配未初始化的内存空间(malloc);2.使用对象的构造函数对空间进行初始化;返回空间的首地址。如果在第一步分配空间中出现问题,则抛出 std::bad_alloc 异常,或被某个设定的异常处理函数捕获处理;如果在第二步构造对象时出现异常,则自动调用 delete 释放内存。
- 执行 delete 实际上也有两个过程:1. 使用析构函数对对象进行析构;2.回收内存空间(free)。
- 以上也可以看出 new 和 malloc 的区别,new 得到的是经过初始化的空间,而 malloc 得到的是未初始化的空间。所以 new 是 new 一个类型,而 malloc 则是malloc 一个字节长度的空间。delete 和 free 同理,delete 不仅释放空间还析构对象,delete 一个类型,free 一个字节长度的空间。
- 为什么有了 malloc/free 还需要 new/delete? 因为对于非内部数据类型而言,光用 malloc/free 无法满足动态对象的要求。对象在创建的同时需要自动执行构造函数,对象在消亡以前要自动执行析构函数。由于 mallo/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行的构造函数和析构函数的任务强加于 malloc/free,所以有了 new/delete 操作符。
C++ 面向对象的三大特征是:封装、继承、多态。
- 所谓封装
就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。 - 所谓继承
是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或者“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”和“组合”来实现。
继承概念的实现方式有两类:
实现继承:实现继承是指直接使用基类的属性和方法而无需额外编码的能力。
接口继承:接口继承是指仅使用属性和方法的名称、但是子类必需提供实现的能力。 - 所谓多态
就是向不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法)。即一个接口,可以实现多种方法。
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
简述指针和引用的区别
- 指针和引用都是一种内存地址的概念,区别呢,指针是一个实体,引用只是一个别名。
- 在程序编译的时候,将指针和引用添加到符号表中。
- 指针它指向一块内存,指针的内容是所指向的内存的地址,在编译的时候,则是将“指针变量名-指针变量的地址”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷贝和赋值,有 const 和非 const 区别,甚至可以为空,sizeof 指针得到的是指针类型的大小。
- 而对于引用来说,它只是一块内存的别名,在添加到符号表的时候,是将”引用变量名-引用对象的地址”添加到符号表中,符号表一经完成不能改变,所以引用必须而且只能在定义时被绑定到一块内存上,后续不能更改,也不能为空,也没有 const 和非 const 区别。
- sizeof 引用得到代表对象的大小。而 sizeof 指针得到的是指针本身的大小。另外在参数传递中,指针需要被解引用后才可以对对象进行操作,而直接对引用进行的修改会直接作用到引用对象上。
- 作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。
野(wild)指针与悬空(dangling)指针有什么区别?如何避免?
- 野指针(wild pointer):就是没有被初始化过的指针。用 gcc -Wall 编译, 会出现 used uninitialized警告。
- 悬空指针:是指针最初指向的内存已经被释放了的一种指针。
- 无论是野指针还是悬空指针,都是指向无效内存区域(这里的无效指的是”不安全不可控”)的指针。 访问”不安全可控”(invalid)的内存区域将导致”Undefined Behavior”。
- 如何避免使用野指针?在平时的编码中,养成在定义指针后且在使用之前完成初始化的习惯或者使用智能指针。
结构体内存对齐方式和为什么要进行内存对齐?
- 首先我们来说一下结构体中内存对齐的规则:
对于结构体中的各个成员,第一个成员位于偏移为 0 的位置,以后的每个数据成员的偏移量必须是 min(#pragma pack() 制定的数,数据成员本身长度) 的倍数。
在所有的数据成员完成各自对齐之后,结构体或联合体本身也要进行对齐,整体长度是 min(#pragma pack()制定的数,长度最长的数据成员的长度) 的倍数。 - 那么内存对齐的作用是什么呢?
经过内存对齐之后,CPU 的内存访问速度大大提升。因为 CPU 把内存当成是一块一块的,块的大小可以是 2,4,8,16 个字节,因此 CPU 在读取内存的时候是一块一块进行读取的,块的大小称为内存读取粒度。比如说 CPU 要读取一个 4 个字节的数据到寄存器中(假设内存读取粒度是 4),如果数据是从 0 字节开始的,那么直接将 0-3 四个字节完全读取到寄存器中进行处理即可。
如果数据是从 1 字节开始的,就首先要将前 4 个字节读取到寄存器,并再次读取 4-7 个字节数据进入寄存器,接着把 0 字节,5,6,7 字节的数据剔除,最后合并 1,2,3,4 字节的数据进入寄存器,所以说,当内存没有对齐时,寄存器进行了很多额外的操作,大大降低了 CPU 的性能。
另外,还有一个就是,有的 CPU 遇到未进行内存对齐的处理直接拒绝处理,不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。所以内存对齐还有利于平台移植。
简述weak_ptr
- weakptr 是一种不控制对象生命周期的智能指针,它指向一个 sharedptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。
- weakptr 只是提供了对管理对象的一个访问手段。weakptr 设计的目的是为配合 sharedptr 而引入的一种智能指针来协助 sharedptr 工作,它只可以从一个 sharedptr 或另一个 weakptr 对象构造,,它的构造和析构不会引起引用记数的增加或减少。
- weakptr 是用来解决 sharedptr 相互引用时的死锁问题,如果说两个 sharedptr 相互引用,那么这两个指针的引用计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和 sharedptr 之间可以相互转化,sharedptr 可以直接赋值给它,它可以通过调用 lock 函数来获得sharedptr。
- 当两个智能指针都是 sharedptr 类型的时候,析构时两个资源引用计数会减一,但是两者引用计数还是为 1,导致跳出函数时资源没有被释放(的析构函数没有被调用),解决办法:把其中一个改为weakptr就可以。
说一下 C++ 里是怎么定义常量的?常量存放在内存的哪个位置?
对于局部常量,存放在栈区;
对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;
字面值常量,比如字符串,放在常量区。