前4章
void Tele::OnOff(TV& t){t.on_off = (t.on_off == true)? false: true;};
void Tele::OnOff(TV& t){t.on_off = (t.on_off == t.on)? t.off: t.on;};(原文,类声明中有enum{on,off};)
三目运算代替if,置位,复位目的
注意,on_off是TV类的私有成员,TV类声明中将自己暴露给Tele,即
friend class Tele;
std::endl与'\n'区别是前者除了换行还调用输出流的flush函数,立即将数据写入文件或屏幕
强制类型转换
(float)5/8;值0.625
<font color=red>std命名空间声明在此 </font>
D:\Microsoft Visual Studio 14.0\VC\include\yvals.h
#if defined(__cplusplus)
#define _STD_BEGIN namespace std {
#define _STD_END }
#define _STD ::std::
在<iostream>中用到相关宏
_STD_BEGIN
cout;
cin;这些对象
...
_STD_END
最麻烦
std::cout << ...;
次之
using std::cout;
最偷懒,但最易引起冲突
using namespace std;
因为以上依次在释放作用域,释放得越广,冲突可能越大
宜将using namespace zhou;放到函数作用域内,效果就是将相关名字只释放到函数域内
没有谁会将函数的返回值修饰成& ,同时将函数局部域变量返回
分割线就是***
分割线也是---
函数声明只是告诉编译器有这么个函数存在,并不分配内存,只有在函数定义时才分配内存
函数的声明,定义分开在不同的文件(.h与.cpp),是种好习惯
原因是:声明和定义在一起,这种必是在一个文件中,缺点是其他文件,模块想使用该方法,就需要先包含函数的声明,即包含该声明定义一体的文件,其实就造成了另一模块也有该函数的定义,即重定义
要解决这个,除非让想使用该函数的模块只包含函数的声明就得行
任何定义在函数外部的变量即全局变量
变量初始化的目的,便于检查,比如年龄不能为0,另者就是作为默认值
定义变量才分配内存,为变量名分配内存地址,声明不会
#include <locale>
**setlocale9(LC_ALL, "chs"); //设置中文简体**
**wchar_t wt[] = L"中"; //L告诉编译器为"中"分配两个字节空间**
wcout << wt;
p27变量所占空间
sizeof()求出数据结构占用的内存大小
cout << "sizeof int: " << sizeof(i) << endl; //4Byte
cout << "sizeof string: " << sizeof(str) << endl; //32位工程:24Byte,无论string内是否有数据,都是24Byte,按此逻辑,string内部对字符的管理是char*;而由于指针在64位工程下是8Byte,所以string在64位工程下是32Byte,string内部管理两个char*
cout << "sizeof pointer: " << sizeof(p_i) << endl; //指针在不同的工程上位宽不一致,64位8Byte,32位4Byte
cout << "sizeof bool: " << sizeof(b) << endl; //1Byte
cout << "sizeof long: " << sizeof(l) << endl; //4Byte
cout << "sizeof double: " << sizeof(d) << endl; //8Byte
cout << "sizeof char: " << sizeof(ch) << endl; //1Byte
溢出不会报错,又会从最小的开始计数
3种float
float; double; long double;
float 精度只有6-7位,即小数点后6-7,double可达到15位
设置输出精度
#include <iomanip>
float a = 12.3332323123344332232;
cout << setprecision(15) << a;
第5章 语句与逻辑表达式
用于计算的操作都可看作表达式,表达式实际是调用函数,表达式总能返回一个值
三个典型的表达式
double PI = 3.1414;
PI; //表达式
1; //亦表达式
//空语句
;
//块,用来存放多条语句
{
;
;
}
求模%,常被放在循环里,用来间隔n次执行,降低了循环频率
//常放在循环里,count亦可在循环外,就不必声明成static
static uint32_t count = 0;
if(0 == count++ % 6) //间隔6次满足条件
{
}
前置与后置的例子
int i = 9 ;
std::cout << ++i; //10
std::cout << i++; //10
关系运算符用来对两个表达式比较,根据比较结果返回真或假,6种关系运算符
==
!=
<
>
<=
>=
逻辑运算符
&&
||
!
三目运算符
z = (a>b)?a:b;
//?:三目运算符,唯一一个需要三个操作对象的运算符
//三目运算符执行优先级,从右到左
z = a>b?a:a>b?a:b;
//等价
z = a>b?a:(a>b?a:b);
大写字母转小写字母
char a;
a = ('A' < a && a < 'Z')?(a+32):a;
第6章 面向对象
面向对象语言初衷:解决c语言的两大难题-代码可重用性差;维护难
四大特征:抽象,继承,封装,多态
类,结构体是数据结构,所以末尾不是
{
} //这是块
而是
};
Q:声明类不会分配内存,定义类会不会分配内存呢?那类的实例会分配内存吧?
A:声明和定义类并不分配内存,声明类的实例才分配内存,与定义变量一个道理;类所占用的内存大小由它的数据成员的内存大小决定(p104)
Q:类的内存大小?数据成员计作内,函数入参,出参是否计作内?
Q:为什么将数据成员设置成私有?
A:提高安全性,防止错误的输入输出,做访问控制,比如先检查对数据成员的操作是否合规
Q:什么函数宜修饰成内联函数,函数体积小且频繁调用
如果不想让成员函数修改成员变量的值,那么将成员函数的函数体修饰为const,对于不应该修改数据成员的函数宜多用const,帮助查错
const{}
如果已经创建带参构造函数,又想要一个不带参的构造函数,那么就只有手工创建一个
重要概念辨析:
默认构造default constructor并不是"没显式声明,编译器自动提供的构造叫默认构造",而是不带参数的构造叫默认构造
它的状态是:若程序员不显式提供,则编译器自动提供,或者编译器自动为带参构造添加默认的构造代码(比如包含了类,这些默认构造代码位于自定义的构造代码前)
第7章
switch结合到while或if可以持续循环,直到某case被激活,跳出循环
第8章
任何被定义的变量都有地址,意思就是任何被定义的变量都是分配了内存空间的
类型别名
typedef unsigned short int ut;
指针3大用途
- 处理堆中存放的大型数据
- 快速访问类的数据成员和函数
- 别名方式向函数传参
内存的几种形式 - 栈区 2M大小由编译器分配,释放,有函数参数值,局部变量
- 堆区 程序员手工分配释放
- 寄存器区 保存栈顶指针和指令指针
- 全局区(静态区static) 存放全局变量和静态变量(包括全局静态,局部静态).初始化的全局和静态放一块,未初始化的全局和静态在相邻的另一块,程序结束系统自动释放
- 文字常量区 存放常量字符串,系统自动释放
- 程序代码区 存放函数体二进制代码
throw的是放在栈上的局部对象
栈内存有限,效率高(速度快),只在函数内有效,超限会overflow,大数据结构宜放在堆中
堆特点:存储较大数据,生命周期靠new delete控制,堆以链表形式由系统管理
函数入栈简析:
被调函数下一行指令地址入栈
函数参数从右至左入栈
函数局部变量
入栈时,栈顶指针向低地址增长
堆灵活,作者举例,比如一个对象能被多个函数访问,但又不想使其成为全局变量,这时,创建堆中对象较好
堆中内存不具名的好处之一:只有特定类型的指针才能访问特定类型的数据,避免了会有任何试图修改它的非法操作
一块内存空间被delete后不要再delete它,否则会崩溃,一般如下操作,释放掉堆内存后为堆指针赋个空值
if(nullptr != p)
{
delete p;
p = nullptr;
//如果p为nullptr,即使delete p是无风险的
}
指针未被初始化一个内存地址或delete堆中空间后一定要将指针赋nullptr(地址清零)
#include <iostream>
using std::cout;
using std::endl;
int main()
{
int* p = new int(3);
delete p;
long* p1 = new long(9999);
//注意以下操作
*p = 23;
cout << *p1 << endl; //23,重大的内存操作错误
delete p1;
return 0;
}
由于编译器默认将释放掉的内存空间分配给新开辟的空间,所以p, p1指向的空间一致.解决办法就是如上以及使用智能指针,虽然使用空指针可能会崩溃,但崩溃总比找不到错误所在要好,另外此处看出:delete只是说明内存不被程序(指针所独占),但仍然不能阻止指针通过*间址访问内存
int i ;
int p = &i;
p++; //p值每次增加4,因为保存的是int变量地址
char ch;
char* p_ch = &ch;
ch++; //ch每次递增一字节
常量指针,指向内存不能改变,但能改变内存上的值
A* const p = new A;
p++; //错误的,p++后p保存的地址就变了,也不能对p赋值
*p = 77; //ok的
指向常量的指针,可以改变指向,指向不同的内存,但不能修改所指内存上的数据
const A* p = new A;
p->set(77) ; //错误,set()成员函数是个修改成员变量的操作
p = new A; //再来一个堆中对象,ok
指向的内存固定,不允许指向其他内存空间;且不允许修改所指内存上的数据-指向常量的常指针
第9章 引用
引用就是对象别名,就是小老婆,一旦依附某变量就不能改变依附,不能++等改变引用本身的操作
定义引用时需要同时进行初始化
引用虽然造成了多个变量共享同一块内存的现象,但是引用是没有"引用计数"的;指针类型的变量没必要取别名.原对象超出作用域时别名也消失
引用和指针的典型用法是作为函数入参,这样起到扩充返回参数的作用
不可能将引用和指针作返回值,同时原对象(或指向的内存)是栈中的局部变量(内存)
按值传递消耗时间及内存,入参.出参均会以拷贝的方式
对象按值传递会调用复制构造函数,在返回时,传递进来的副本会调用析构系函数来销毁,而如果要按值返回对象,则又会调用复制构造函数,返回完,则调用析构函数销毁出参的对象
可以对堆中空间取别名
int* p = new int(6);
if(null != p)
int& r = *p; //r即为内存空间的别名
r = 10; //即*p = 10;
Q:那么抛出一个问题,p指向的空间被delete后r的状态呢?
A:经过测试,保存的仍然是原空间地址,但实际这是不安全的,和指针delete后的情况一致,即仍指向原空间但并不是专有此空间,有新的new时会将这块已经被释放的空间分配出去
p136
A& r = func();
//fun()函数并非将返回值声明成&,此处是新定义一个引用对象,按值接收函数内返回的副本,这个副本的生命周期会跟随r
//如果引用的是临时变量,变量的生命周期不小于引用的生存期
另一情况,用指针指向临时变量,同样的背景:A func()按值返回
A* p = &func();
//返回的副本立即被销毁(调用析构函数),p指向的空间具有不确定性,不是p所独占
引用和指针作返回值的用例,按引用或指针返回一个堆中创建的空间
但此法不安全,不满足在哪创建就在哪销毁的原则,宜使用智能指针来接收堆中对象
譬如声明A& func(),使用时需要定义一个别名来接收
A& ra = func();
//值得品味的是下面这种,不会调用赋值=运算符函数,调用的是复制构造函数,这是编译器自己发现了用值接收,所以按值返回
//**如此会造成内存泄漏,按拷贝返回出来,a1其实得不到堆中地址的**
A a1 = func();
引用和原变量关联的地址相同,说明,只是不同的符号,关联相同的地址
int main(int argc, char** argv)
{
uint32_t i = 0;
uint32_t& ri = i;
cout << "&i 表示i这个符号绑定的内存,这块内存的地址:" << "\t" << &i << endl;
cout << "&ri表示ri这个符号所关联的内存, 这块内存的地址:" << "\t" << &ri << endl;
system("pause");
return 0;
}
第10章 深入函数
掌握深层复制,运算符重载,要徒手写几个运算符重载的函数
一切运算执行均是调用函数,要重视
全局函数的默认值参数初始化
void func(int = 0, int = 0); //声明
同样,类的成员函数也可以有这种声明
Q:只为一个参数指定默认值可以不?
A:参数只有一个指定了默认值是可以的,但是,该默认参数应在后面,否则在它后面还有未指定默认值的参数会报错
void set(int = 100, int = 200); //ok
//void set(int _j, int _k = 200); //ok
//void set(int _j = 100, int _k); //error
参数初始化列表,唯三必须用参数初始化列表的
- 成员变量是引用
- const成员的初始化
- 成员变量是未提供默认构造函数的类类型
class A
{
A(int x, int y):total(x), num(y) {}
private:
int& total;
const int num;
};
成员变量的初始化顺序与构造函数中初始化顺序无关,只与在类中的说明顺序有关,析构函数的析构顺序正好相反
构造函数是在初始化成员变量,所以,如果有包容其他对象,那么,会先执行包容对象的构造,然后执行自身的构造,析构时顺序正好相反,先析构自身呢个,再析构包容的对象
按值传递对象会调用复制构造函数
A::A(A& ra);
参数是类A的引用,那么就可以通过该引用来访问它的对象,引用只能被初始化,不能被赋值,把引用说明成常量引用是非常好的主意,构造函数不必改变传进来的对象
//实现,浅拷贝,若p为指针
A::A(A& ra)
{
this-> p = ra.p;
}
//深拷贝
A::A(A& ra)
{
*(this-> p) = *(ra.p);
}
每个类都有一个默认复制构造函数,使用引用来访问指定对象的内存地址,然后复制该对象的成员变量到自己的成员变量
默认构造函数,它不是将数据成员初始化成默认值,数据成员是未初始化的
把数字当作对象赋给另一对象,将数字进行类型转换,一个重要的操作,判断该类的构造函数的参数是否与数字的类型匹配,假如匹配则调用构造函数创建临时对象,跟着将该临时对象赋给=操作符左边的对象,最后会调用析构函数销毁临时对象
A a(99);
a = 100; //这步会调用构造函数(参数类型能与int匹配的构造函数,创建临时对象),operator=()函数(临时对象赋给左边),析构函数(销毁临时对象)
a = A(2); //与上例一样的过程
/*
上两例执行结果
1. A& operator=(A& ra) { cout << "operator=()函数执行" << endl; return ra; } //参数必须是A或A&,返回值可以是A&或void或A
构造函数执行
构造函数执行
operator=()函数执行
析构函数执行
析构函数执行
2. A operator=(A& ra) { cout << "operator=()函数执行" << endl; return ra; }
构造函数执行
构造函数执行
operator=()函数执行
复制构造函数执行
析构函数执行
析构函数执行
析构函数执行
3. A operator=(A ra) { cout << "operator=()函数执行" << endl; return ra; }
构造函数执行
构造函数执行
operator=()函数执行
复制构造函数执行
析构函数执行
析构函数执行
析构函数执行
4. A& operator=(A ra) { cout << "operator=()函数执行" << endl; return ra; }
构造函数执行
构造函数执行
operator=()函数执行
析构函数执行
析构函数执行
*/
explicit A(int x){}; 关键字会关闭这种转换特性
Q:何时调用=y运算符函数,何时调用复制构造函数?
A:按值传递参数时调用复制构造函数
默认的复制构造函数都是搞的浅拷贝,要实现深拷贝就要自己创建复制构造函数,深拷贝后两个对象都有各自的内存区域,互不干扰
第11章运算符重载
class A
{
public:
A(int _i): _i(i){}
void operator++(); //前置自加运算符函数
public:
int i = 0;
}
A a;
++a; //调用operator++()
来几个实验,operator++()的返回值声明成A
//对象如上例,只是变A operator++()
1.
A operator++()
{
++i;
A t(i); //返回临时对象
return t;
}
2.
A operator++()
{
++i;
return A(i); //返回匿名临时对象
}
3.
A operator++()
{
++i;
return *this; //返回对象自身,减少了上面两步中的临时对象,减少资源占用,但它是按值返回,同样需要调用复制构造函数
}
4. 为了解决上面按值返回,将返回值修饰成&,并且不可能执行++++a;这样的操作,所以有必要将返回值定义为常量
const A& operator++(){}
后置自加运算符函数operator++(int _i),带一个参数与前置自加相区别
const A operator++(int _i){A temp(*this); ++i;return temp;}
返回值修饰成const的意义不大,只是说返回值不能立即修改,而接收它的对象可以修改
加法运算符函数 operator+()
A a,b;
a + b; //即a.operator(b);
//由此,可以推断operator+()的定义
const A& A::operator+(A& a)
{
return A(i+a.i);
}
赋值运算符函数operator=(& r),默认调用它,会将对象b的成员变量复制到对象a中去,即不可避免地调用复制构造函数(经过验证,确实如此,默认进行的是浅拷贝)
A a,b;
a = b; //即a.operator(b);
赋值,涉及到重要的问题,就是浅拷贝还是深拷贝,浅拷贝就是将指针值(地址)拷贝,深拷贝是将内存上的数值拷贝
重要问题,思考下下面这段代码是否安全
//A.hpp
#pragma once
#include <iostream>
using std::cout;
using std::endl;
class A
{
public:
A() { p = new int; }
A(int _i) { p = new int(_i); }
~A() { delete p; cout << "析构函数执行" << endl; }
//升级后的复制构造函数
A(A& ra) { p = new int; *p = *(ra.p); }
A operator=(A& ra) { *p = *(ra.p); return *this; }
int get() { return *p; }
private:
int* p = 0;
};
//main.cpp
#include "A.hpp"
#include <iostream>
using std::endl;
using std::cout;
int main(int argc, char argv[])
{
A a(5);
A a1 = a; //operator=()执行时会调用默认的复制构造函数,然后调用析构函数,析构时副本的地址空间和原对象空间一致,所以析构出问题
//解决办法就是升级(重载)复制构造函数,即A(A& ra) { p = new int; *p = *(ra.p); }
cout << a1.get() << endl;
return 0;
}
//A:崩掉,原因在A a1 = a;时,析构副本时将原对象中p指向的空间析构,导致main函数结束时无法析构原对象p所指空间,崩溃
可见operator=()函数和A(A& ra)复制构造函数都必须考虑深浅拷贝的问题,坏就坏在operator=()会不可避免地调用复制构造函数(要按值返回的嘛)
解决办法之二:
A& A::operator=(A& ra){*p = *(ra.p); return *this;}
//将返回值修饰成引用,就不会调用复制构造函数
解决办法之三:
//将返回值修饰成void,不返回,自然不会调用复制构造函数
void operator=(A& ra) { *p = *(ra.p);}
以上这波是operator=()要不要调用复制构造函数?及复制构造函数用深浅拷贝的问题?
Q:如果是自己赋给自己a=a;有没破绽?
A:由于赋值运算符必须先释放掉旧值,然后根据新值分配数据,当对象a.operator(a),左侧a释放p指向的内存,然后根据右侧参数a的指针p指向的内存来给左侧a赋值时就出问题了
所以在operator=()中添加一句
if(this == &ra)
return *this;
通过operator关键字进行类型转换,operator配合要转换的类型,构成一个重载运算符函数
转换运算符的重载函数没有返回值,这点与构造函数,析构函数类似.该函数虽然没有返回值,但仍可以返回值,返回类型根据operator后面的类型符定
A::operator float() { return (float)*p; }
//main.cpp中使用
A a(67);
float f = a;
利用该函数完成对象->变量的转换
标准的赋值运算符函数,看它的返回类型是&
//operator=()函数,深拷贝
movedemo& operator=(const movedemo& r) { cout << "operator=() " << endl; num = new int(*(r.num)); return *this; }
按引用传入,返回,但其成员变量不是用深拷贝的话,成员变量的赋与以按值的形式
不可被重载的运算符
# //预处理
:: //域限定符
* //指针运算符
. //成员选择符
?: //重载没意义
第12章 继承
#include <iostream>
using std::endl;
using std::cout;
class A
{
public:
A() { cout << "A() run " << endl; }
//virtual ~A() { cout << "~A() run " << endl; } //解决办法将它声明成virtual 在delete时会多态性执行派生类的析构函数
virtual ~A() = default;
};
class B : public A
{
public:
B() { cout << "B() run " << endl; }
~B() { cout << "~B() run " << endl; }
};
int main(int argc, char* argv[])
{
A* b = new B;
delete b;
return 0;
}
思考:如果基类的析构函数不声明成virtual 会发生什么重大错误?
A:只会调用基类的析构函数不会调用派生类的析构函数
作者言中了,的确基类指针保存派生类对象的后果要考虑析构;要充分释放资源
//www.greatytc.com/p/ef66ba66916c
私有成员无论何种继承(公有,私有)都只留有本类成员和友元接口(方法)
class A public: B
上句中的public是访问类型
派生类对象可以赋给基类对象,派生类对象地址可以交由基类指针保管(多态),派生类对象可以初始化基类引用(多态)-以上三种反过来不得行
私有继承方式中,基类中的public和protected成员在派生类中的访问权限变为private,对派生类而言,它仍能访问这些成员(私生子仍能享有这些财产),但不能将这些接口暴露出去,派生类对象要使用这些接口必须先定义公有成员作为接口,在公有接口中调用私有成员(使用这些财产略显麻烦)
私有派生不利于继续派生,实际使用不多
Q:派生类不显式构造基类部分,基类会如何?会执行哪个构造函数?
A:
派生类初始化基类部分两种方式:
- 派生类构造函数初始化所有数据包括基类部分的,这种称之为多此一举,因为基类的构造函数已经做了这个工作
- 派生类构造函数参数初始化列表,或以调用形式调用基类构造函数
析构,就不需要显式地调用基类的析构函数
多重继承构造按继承时声明顺序执行
多重继承,基类的成员名相同的话会产生二义性,解决办法就是作用域运算符来起限定域的目的
C c;
c.a::print();
c.b::print();
派生类与基类有同名,或者是同名(同签名,如const{})且同参函数时,派生类将基类的同名(同签名)同参函数覆盖,将派生类同名函数隐藏
覆盖支持多态,隐藏破坏多态性
一个类从多个基类派生,而这些基类又有共同的基类,那么在派生类中访问共同的基类成员时会产生二义性
解决办法,声明为虚基类
//将最高基类说明成虚基类
class A : virtual public B
{};
基类指针不宜强制转派生类指针,若要强转,需要考虑安全与异常处理,即调用dynamic_cast<>,转换失败会抛出异常,虚基类的指针或引用则是不能转派生类指针引用,原因在于虚基类在内存中布局所致
第13章 虚函数
第12章开头的警示代码需要注意,思考如何解决
C++多态性的特征之一就是允许把派生类对象赋给基类指针,并使用该指针访问基类的数据和函数
virtual表明该函数有多种形态,该函数可能被多个对象所拥有
- 指针(一般是基类指针)在编译时不确定是指向派生类对象,还是基类对象.在运行时才确定的叫动态联编或叫运行时联编;
- 在编译时就确定好了的叫编译时联编或叫静态联编
(这种确定以在编译时确定对象还是在运行时确定对象的方式可以取名为对象联编)对于静态联编,对象不用对自身进行跟踪,速度浪费比较小,动态联编可跟踪对象,灵活性强,速度浪费严重
- 在编译时就确定好了的叫编译时联编或叫静态联编
- 不使用virtual的情况下C++对重载的函数使用静态联编-调用函数和被调函数之间的关系在编译时就确定好
-
使用virtual后,对重载函数使用动态联编-跟踪对象,根据实际的对象联结对应的函数
Q:这里讲的动态/静态联编是什么呢?
A:这里讲的是函数联编,将调用函数连接上正确的被调函数称函数联编,简称联编
最准确的叫法,静态联编-调用函数和被调函数的关系是确定不变的;动态联编(virtual修饰)-调用函数和被调函数的关系要跟踪对象来联结对应的函数
注意:
-
使用virtual后,对重载函数使用动态联编-跟踪对象,根据实际的对象联结对应的函数
1. 析构函数不允许有参数,即不能实现重载,一个类只能有一个虚析构函数
2. 基类的析构函数必须声明为虚函数(派生类部分才会被析构),派生类的析构函数无论说明与否,都自然成为虚函数
3. 虚构造函数不存在
虚函数表参考:
https://leehao.me/C-%E8%99%9A%E5%87%BD%E6%95%B0%E8%A1%A8%E5%89%96%E6%9E%90/
至此,牢记一个概念:实际上所有的函数都存放在单独的一个代码区,而函数对象里面只有成员变量。
留个问号,何为隐藏/覆盖?
第14章 数组
数组是一组相关的内存位置,他们具有相同的名称和类型
数组特征三点:类型,数组名,下标
算法:
- 斐波那契数列,一对兔子,出生后第一个月成年,第二个月有生殖能力,有生殖能力的兔子每个月都生一对兔子,假设生出的兔子无死亡,那么一年后有兔子好多对?
- 排序问题,实际就是双层的遍历
输出数组名就是内存地址
int a[4] = {1, 2, 3, 4};
cout << a << endl;
//0013ff70,实际即 &a[0]
声明数组时编译器会自动生成指向该数组的指针,该指针通常保存数组第一个元素的内存地址,数组不能按值传递给函数,但却能将内存地址传递给数(C++规定数组只能有一个原本)
函数接收数组的形参声明:
//1
void func(int a[])
//2
void func(int a[30])
//3
void func(int* a)
- 在有序数列中按二分法查找目标
二分法缺点:1. 数组中有两个或两个以上相同的元素,二分算法无法确定返回哪个值;2. 二分算法要求数组有序
将对象数组放在堆中
A* p_a = new A[10];
delete []p_a;
二维数组
a[rows][cols]
a[2][4] = {{1,2,3,4},{5,6,7,8}};
字符串数组
char ch[] = "hello world";
char ch1[20] = {"hello world"};
cout << ch ; //能将字符串完整输出
字符串输入,引入下面问题
char a[20];
cin >> a;
//并不会完整接收"hello world",因为cin>>遇到空格停止向缓冲区写入
解决办法:
gets(a);
在上例中输入字符串超过定义的空间会数组越界,解决办法:
//手工限制输入的字符个数
cin.get(a, 20);
目标数组必须足够大,容得下经过copy或strcat后的字符串
字符串拼接
//1.strcat(目标,源)
char a = {'"my name is "};
char b = "jack";
strcat(a, b);
//2.copy(目标,源)
strcpy(a, b);
//3.小写字母转大写
strupr(a);
//4.大写转小写
strlwr(a);
//5.字符串长度
strlen(a);
注意:二维数组传指针到函数里,涉及到的重要操作
//这种方式取得二级指针
for (int i = 0; i < 5; ++i)
p_array[i] = &maritx[i][0];
int** pp_array = &p_array[0];
int* p_maritx = &maritx[0][0];
printf("num[x]: %d \n", p_maritx[24]);
//下面是错误操作
int** pp_maritx = &p_maritx;
printf("num[x][x]: %d \n", pp_maritx[3][3]); //栈溢出
字面值常量不属于可修改的内存区,其实是符号常量区
#include <iostream>
using std::cout;
int main(int argc, char** argv)
{
/*char* p = "我是字面值常量";
char* p1 = "我是字面值常量";*/
char* p = "hello word";
char* p1 = "hello word";
cout << "&p: " << &p << "\n";
cout << "&p1: "<< &p1 << "\n"; //两者地址不同
//*p = "哈哈,已改变"; //不能修改,其实这块"内存"是特殊的内存,不能再写了,符号常量区
std::cin.get();
return 0;
}
第15章 动态链表
c语言形式申请动态内存
void* p = malloc(sizeof(struct book));
free(p);
句柄本质是什么?是二级指针.为什么一级指针就能索引的内存还需要搞成二级指针来索引?
因为系统存在虚拟内存(磁盘上某部分swap),内存上数据可能需要经常迁移往返虚拟内存,这意味着内存地址在经常变化,win为解决这个问题,专门腾出来一部分内存空间来放内存变化的地址,所以,二级指针相对固定,一级指针可能经常变化
第16章 多态性
dynamic_cast基类指针转派生类指针,有风险,经他转换的指针有风险,转换后的基类指针只能访问派生类部分
数组也可认为是链表
父类,母类继承自共同的基类-人类,子类继承自父.母类.父母类不说明成虚继承的话,子类就会执行人类的构造函数两次,析构函数两次,即有人类部分两次
很多时候,基类的虚函数不会实例化,只是作为向下传递的接口,派生类也不一定要实例化虚函数,有时只是作为向下传递的接口
类的静态成员函数不能访问类的数据成员(没有this指针),只能访问静态变量,静态成员变量必须初始化
静态成员函数不能说明成虚函数
成员函数指针声明
float (A:: *pf)(int, int);
//甚至可以说明成抽象类的函数指针
float (A:: *pf)(int, int) = 0;
cin.get()的结束标志'\n'换行符,会自动为输入字符串添加'\0'结束符
第17章 类的特殊成员
第18章 字符串
char型字符数组
char[5] = {'h', 'e', 'l', 'l', 'o'};
char型字符串
char[6] = {'h', 'e', 'l', 'l', 'o', '\0'};
strlen() //返回sizeof()-1,即不包括结束符
有了C++就有了类,就有了C++风格的字符串,即string类型的字符串
strcmp(); //比较两字符串,相等返回0
strcpy(dst, src);
operator=(); //直接调用赋值运算符
dst.assign(src, 3, 1) //拷贝源端任意区间字符串覆盖dst端src索引为3开始的1个字符串拷贝到src
char dts[] = {};
char src[] = {};
strcat(dts, src); //字符串拼接,char[]型字符串必须保证dts有足够的空间
dts是string型字符串就没此问题
//亦可直接调用
operator+()函数
//strlen()是C语言函数不支持对象,所以对象使用它
strlen(str.c_str()); //输出不包含结束符的字符个数(包括可见字符,除结束符外的控制符)
str.leng();
str.size();
//两者效果相同,输出字符串个数,不包括末尾结束符
//约定:字符串元素个数即完整包括结束符的个数
//字符串的部分合并
strncat(dts, src, n); //src的前n字符拼接到dts尾
字符串部分拼接
//append()第2,3参数使用
dst.append(src, 2, 3); //src的索引为2,连续3个字符串拼接到dst尾
//C风格的将src中前n字符覆盖dst
char dst[] = {};
char src[] = {};
strncpy(dst, src, n);
//字符串交换奇偶字符
swab(src, dst, strlen(src));
//string非成员函数交换字符串
swap(a,b)
字符串无论string抑或char型字符数组,他们的重要标志是有结束符'\0'
同样是字符串,但本质不一样, 一个是字符数组,另一个是对象
string str = "hello";
char* p_ch = str; //错误
char* p_ch1 = "hello"; //正确
鲜有将指针作为函数返回值的
分析:
- 1 不可能是在函数内的栈中对象的地址,函数结束时,局部变量被销毁,自然返回局部变量地址是无意义的
- 2 若是堆中的动态内存,又破坏了在哪创建在哪销毁的规则,解决函数域内创建,要传递到函数外的方法是动态内存按智能指针返回
用命令行将输出重定位到文件a.exe >> out.txt调试时有用
重载<<运算符,将iostream对象引用和自定义的对象引用当作参数传递进函数,可以将输出cout或cin重定向
//p405第一次出现友元的说明
ostream& operator<< (ostream& in, A& a) //声明
//修饰成友元亦可,将该类的成员暴露给ostream对象
friend ostream& operator<< (ostream& in, A& a)
p398 结构体的方式计算两天的时间总和
第19章 代码重用
p449
const 对象只能调用const函数
例如
const String a;
String operator+(const String&) const;
//a可以调用上面函数,因为最后一个const修饰函数体可以被const对象调用的函数
包含对象,则会先调用数据成员的构造函数,构造包含的对象,析构时会先调用外层的析构函数,然后析构被包含的对象
两个case都落到一种情况
//p466
case same:
case larger:
{}
p466
对insert()来说只可能返回两种指针:
1:指向新节点的指针
p462 17行
头节点接收前插法创建的新节点的指针
p466 13行
递归调用,下一节点所创建的新节点2.this指针
递归调用时同样在p462 17行及p466 13行接收
这种情况就是,递归的不创建新节点,保持原来指向的下一节点不变
注意,这种递归思想学习下
p496介绍继承的几种分类区别
第20章 友元类与嵌套类
根据原值将bool值反转,三目运算符
t.on_off = (t.on_off == TV_ON)? TV_OFF : TV_ON;
第21章 流
iostream输入输出流将输入或输出看作逐个字节的流,输入时输入对象将逐个字节注入到变量或内存中,输出时流对象将逐个字节输出到屏幕或文件中
第22章 命名空间
命名空间主要解决命名冲突即重名问题
可以多次的,重复的,在不同文件的,创建同名的命名空间
尽量在函数声明处声明命名空间,不要在函数定义处声明命名空间,目的:1.有利于命名空间的整洁※;2.有利于将命名空间存放在头文件中,函数定义部分放在实现文件中
命名空间也可以嵌套,使用的时候就要使用多重::来释放命名空间
using关键字能将命名空间从该using声明处释放至该作用域结束
作用域中定义的同名变量会覆盖命名空间中的变量;类似小域(局部域)变量覆盖大域(全局域变量)
为命名空间取别名
namespace long_name
{}
namespace short_name = long_name;
//创建的别名不能与其他的命名空间名字重复
未命名的命名空间
namespace
{}
//命名空间都要显式地声明,但名字可以没有
未命名的命名空间,编译器会为每个命名空间分配名字,每个文件中名字都不一样.它有这么两个特点:1.该文件中访问该未命名的命名空间中对象会自动添加命名空间名来访问,即直接访问命名空间中的变量;2.其他文件不能访问该命名空间中的变量. 基于此,推荐之前用的文件域内的static改为namespace{} (未命名命名空间中的所有成员在其他文件中不可见)
1.cpp 2.cpp中分别定义变量int x = 10;会产生变量重定义错误
extern与static恰恰相反,static将变量限制在所在域内(文件级或某{}内);而extern是声明在外部文件定义的对象;static是内部链接,extern是外部链接,未命名的命名空间也是外部链接
用namespace而不用static的重要意义在"假如将全局对象和函数放心地扔进匿名空间,并将它地址作为模板参数,那么就要求这个匿名空间必须是外部链接,否则模板就没有多大的意义.由于static是内部链接,仅在当前文件可见,其他文件无法对其进行引用,因此,无法将模板实例化"
第23章 模板
template<typename T>
T max(T a, T b)
{
return(a>b)?a:b;
}
要求:1.元素可排序;2.重载了operator>()运算符;对处理数据有要求的函数模板称为约束函数模板
p610具体化函数模板解决重载
template<> void Swap<people>(people& p, people& p1);
//告诉编译器放弃函数模板,使用具体化了(首先必须是具体化了参数类型)的模板,这么做毫无意义,实际就在这步就会生成函数实体,等价于
void Swap(people& p, people& p1)
模板定义在编译前不会生成函数实体,模板只是生成函数实体的方案
类型推导
//两个都实现了按引用传入对象的目的
#include <iostream>
template<typename T>
void fun(T _in)
{
_in = 'W';
}
template<typename T>
void fun1(T& _in)
{
_in = 'J';
}
int main(int argc, char* argv[])
{
char in = 'A';
//fun
fun<char&>(in);
std::cout << "in: " << in << std::endl; //W
//fun1
fun1(in);
std::cout << "in: " << in << std::endl; //J
system("pause");
return 0;
}
继承和模板都可以派生出一个类系,实现代码重用的目的,但两者又有本质区别
模板生成的多个新类.实现的是对多个数据类型的重载,它们处理数据的方法是相同的.但继承方式派生的类,数据有增加的可能,而且对数据的操作方式也可能会有变化,比如说派生类会提供更多的方法对数据进行操作,或改变原有方法,使得数据的操作更加简单和快捷.而且这些类之间也存在兄弟,父子之间的关系
p637 23.14节 将模板用作参数,这个看代码好理解
#include <iostream>
#include <string>
using namespace std;
template<class T>
class human
{
public:
human(){}
T GetAge(){return age;}
T GetStr(){return name;}
void SetAge(T &a){age=a;}
void SetStr(T &s){name=s;}
private:
T name;
T age;
};
template<template<class T>class T1>
class people
{
public:
people(){}
int GetAge(){return s1.GetAge();}
void SetAge(int &a){s1.SetAge(a);}
string GetStr(){return s2.GetStr();}
void SetStr(string &s){s2.SetStr(s);}
private:
T1<int>s1;
T1<string>s2;
};
int main()
{
people<human>Jack;
int i=8;
string str="hello";
Jack.SetAge(i);
cout<<Jack.GetAge()<<endl;
Jack.SetStr(str);
cout<<Jack.GetStr()<<endl;
system("pause");
return 0;
}
模板类可以声明友元,模板类的友元分三种情况:
- 1.非模板友元类或友元函数
- 2.通用模板类或友元函数
- 3.特定类的模板友元类或友元函数
模板类外部声明(模板友元函数不搞成内嵌函数),特点模板友元函数分3个步骤:
- 1.模板类前面声明模板友元函数
- 2.模板类中具体化模板友元函数
- 3.为特定模板友元函数提供模板定义
23.16多余的临时对象看代码领悟
截止现在对前面章节小结:
- 1.看代码为主了,看书缓一下
主要代码章节:
-18章自己实现的String空了看下,主要是看关键函数,比如赋值运算符函数,要深拷贝;要看如何保存基础的数据,比如数据成员是char* 还是char[]- 20章两个示例工程:友元类,嵌套类
- 21章结合到书来看某些流对象的操作
- 23章目录"用VS2005编译的程序" 23.14-23.16节代码研究清楚 23章后面就是stl的使用了
- 24章主要以看代码为主,25章概念比较多,先看书吧
关联容器是特殊的顺序容器<<C++标准库>>
- 顺序容器-array或list
- 关联容器-binary tree
- 无序容器-hash table
所有顺序容器都有
emplace_back()
//因为向头或中间添加可能会移动整个array,比如vector,所以向头添加可能会造成比较大的消耗,是种特殊操作
关联容器的差别在于处理元素种类以及处理重复元素的方式
set可视作特殊的map即key = value
无序容器的优点是,当查找某特定值元素,其速度可能快过关联容器
map以key为根据来排序
所有无序容器都提供若干可有可无的template,用来指明hash函数和等效比较式
map抑或unodered_map的下标[]运算符和array的不一样,接收一个key,返回键对应的mapped_value,若没此键则建立一个新的元素
也可以用at()来访问mapped_value,这样对容器操作是安全的,若没有这个键key_type,用at()访问的话会抛出out_of_range的异常
推荐使用unodered_map除非想map是有序的
容器适配器提供一定限度的接口,应付特殊需求,适配器都是根据基本容器实现
Priority queue,所谓优先权是基于程序员提供的排序准则而定;容器适配器是种特殊的容器
每种容器都将迭代器以嵌套的方式定义在class内部,每种迭代器接口相同但是类型不同
如果使用非常量迭代器,并且元素类型也是非常量,则可以使用迭代器修改元素值
向set<,>传入第二模板参数,即可调对象,排序将按此规则,例如
set<int, greater<int>> set_instance;
前向迭代器,迭代器只额能递增:forward_list;unordered_set;unordered_multiset;unordered_map;unordered_multimap的迭代器都是前向迭代器
双向迭代器,list;set;multiset;map;multimap
随机访问迭代器:提供了迭代器算术运算的操作符:vector;array;string;deque
template <class T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
//隐式实现模板
int v1 = 1, v2 = 2;
int ret = compare(v1, v2);
19章19.7之前是一种思路,19.7是另一种思路,而19.8是19.7的进阶应用
- 第24章 异常
注意,在构造函数中抛异常,需要在抛之前先delete this
throw的是放在栈上的局部对象
为了从try块中检测出一个异常并跳转到相应的catch块中,C++函数发出throw语句,throw语句的数据类型与某个catch块所接受的参数相匹配,throw用来表示错误已经发生.这样控制语句就从throw语句跳转到catch块中,接着throw语句通过调用析构函数,删除try块中的定义的所有对象
#include <iostream>
using namespace std;
class wrong
{
public:
wrong() { cout << "wrong() run" << endl; i = 55; }
wrong(wrong&) { cout << "wrong() copy construct" << endl; }
wrong& operator=(wrong& _in) { cout << "wrong() assigment construct" << endl; return _in; }
int print() { return i; };
private:
int i;
};
void error()
{
cout<<"出错\n";
throw wrong();
}
int main()
{
try
{
error();
}
catch (wrong& w) //传递的对象以引用形式接收
{
cout<<"该错误已经解决\n";
cout << "传递的wrong对象中i是: " << w.print() << endl;
}
return 0;
}
异常对象是个栈中对象,即throw抛出的对象
自定义assert来处理调试信息
#define DEBUG
#include <iostream>
using namespace std;
#ifndef DEBUG
#define ASSERT(x)
#else
#define ASSERT(x)\
if (!(x))\
{\
cout<<"错误!ASSERT("<<#x<<")宏函数执行检测\n";\
cout<<"错误代码出现在第"<<__LINE__<<"行\n";\
cout<<"出错的文件在:"<<__FILE__<<"\n";\
}
#endif
int main()
{
int x=999;
cout<<"第一次执行assert():\n";
ASSERT(x==999);
cout<<"第二次执行assert():\n";
ASSERT(x!=999);
cout<<"程序结束.\n";
return 0;
}
注意,下面一行的#x会将输入的宏表达式打印出来
cout<<"错误!ASSERT("<<#x<<")宏函数执行检测\n";\
调试级别修改,摘自原文
#include <iostream>
#include <string>
using namespace std;
#define DEBUG 4
#if DEBUG<2
#define ASSERT(x)
#else
#define ASSERT(x)\
if (!(x))\
{\
cout<<"错误!ASSERT("<<#x<<")宏函数执行检测\n";\
cout<<"错误代码出现在第"<<__LINE__<<"行\n";\
cout<<"出错的文件在:"<<__FILE__<<"\n";\
}
#endif
#if DEBUG<3
#define SHOW(x)
#else
#define SHOW(x)\
cout<<x<<endl;
#endif
#if DEBUG<4
#define PRINT(x)
#else
#define PRINT(x)\
cout<<#x<<endl;
#endif
class Circle
{
public:
double check()const
{
SHOW("进行3级检查");
PRINT("进行4级检查");
return radius;
}
void set(double x)
{
ASSERT(check());
radius=x;
ASSERT(check());
}
double Result()
{
return 3.14*radius*radius;
}
private:
double radius;
};
int main()
{
Circle one;
one.set(14);
cout<<"圆的面积为:"<<one.Result()<<endl;
one.set(0);
cout<<"圆的面积为:"<<one.Result()<<endl;
return 0;
}
try
{
dynamic_cast<派生类&>(基类对象);
}
catch (bad_cast)
{
}
//转换不成功dynamic_cast则会返回NULL
//只有基类用到虚函数时才用到动态类型转换,要结合到C++开启RTTI
try
{
new A();
}
catch (bad_alloc)
{
}