目录

一、关于对象

二、深入了解数据成员

三、深入了解函数成员

关于对象

1.类继承时加上virtual关键字就变成了虚拟继承

  • 虚拟继承的作用是保证无论同一父类被该子类重复n次直接或间接继承,该子类的对象中始终只有一个此父类的实例
  • 例如b1虚拟继承a、b2也虚拟继承a,然后c既继承b1又继承b2,此时c类对象也只有一个a类实例

2.C++程序设计模型的三种范式,即每个程序要保证用同一范式思想

  • 程序模型:使用C的语法,用字符数组和str*函数族群
  • 抽象数据类型模型(基于对象OB):也用string等对象,强调数据封装,但不支持多态
  • 面向对象模型(面向对象OO):强调基类、虚函数和动态绑定等概念,支持多态

3.指针的本质是完全相同的,不管它指向的是哪种类型对象

  • 它的内存存的都是一个机器地址,也就是说指针类型会教导编译器如何解释某个特定地址中的内存内容及其大小
  • 例如一个指向地址1000的int指针,在32位机器上将涵盖地址空间1000~1003
  • 例如一个指向地址1000的string指针(包括一个4字节字符指针和一个用来表示字符串长度的整数4字节),那么它将将涵盖地址空间1000~1008,注意string实现方式有很多种,不过一般都不少于8字节
  • 而void*指针只标识了地址并没有说明有多大,因此不能直接通过它操作它指向的内容
  • 因此指针的转换cast其实是一种编译器指令,大部分情况它不改变一个指针所含的真正地址,它只影响“被指出的内存的大小和内容”的解释方式

4.引用也额外需要4字节空间(存在堆上)

5.枚举类型enum默认每个成员占4字节,除非有大于4字节的值那么就以这个大值为准

6.子类对象的占用字节数等于子类的字节+父类的字节之和

  • 因为规定对象的大小必须大于0,所以一个空类的对象占用1字节(编译器会给它安排一个char),但如果是继承空类的子类对象的话那么占用字节就应该等于子类的字节+0(有些老的编译器也还是会+1)

7.只有用new定义的对象才在堆里面,否则定义类对象和定义变量一样都存在栈里面

8.对象、变量放栈和堆的选择问题

  • 堆空间大,适合放数组以及类对象(系统自带或自定义)
  • 堆里面的数据不会自动清空,这样就可以利用指针来访问,(自动清空数据恰好是栈的优点)。所有在使用的时候要辩证来用,如果是用指针来访问,就用堆,千万不能用栈
  • 效率低,因为堆空间大,所以相对来说效率会低一点
  • 使用堆长期运行,有可能会导致内存碎片问题(请求一个43字节的内存块时,因为没有适合大小的内存,所以它可能会获得44字节,因此由所需大小四舍五入而产生的多余空间就叫内部碎片,外部碎片是频繁分配和回收物理页面导致小的页面块夹在已分配的页面中间而用不上)

9.虚继承也会有虚指针占用空间

1
2
3
4
5
6
7
8
9
10
class X { }; //X占1字节,编译器给它加的char相当于一个标识
class Y : public virtual X { }; //Y占4字节,4字节来自虚函数指针
class Z : public virtual X { }; //Z占4字节,4字节来自虚函数指针
class A : public Y, public Z{ }; //A占8字节,8字节来自Z和Y分别含有的虚函数指针

//如果是旧的没有处理虚基类的编译器
class X { }; //X占1字节,编译器给它加的char相当于一个标识
class Y : public virtual X { }; //Y占8字节,4字节来自虚函数指针+1字节X+3字节补齐
class Z : public virtual X { }; //Z占8字节,4字节来自虚函数指针+1字节X+3字节补齐
class A : public Y, public Z{ }; //A占12字节,1字节X+Y自己本身的4字节+Z自己本身的4字节+3字节补齐

8.计算类的占用字节数也会补齐字节

  • 已知A类有一个char成员占1字节,B类继承自A类并自己有一个int成员
  • 则B类对象占用8字节,来源于char1+int4=5,然后要为4的倍数则补上3字节,最后等于1+4+3=8

9.windows64位可以看成32位的标准,因为

  • windows64位一般使用LLP64模型
  • 64位Unix,Linux使用的是LP64模型

10.判断一个类的占用字节,一定要考虑以下两点

  • 因虚函数和虚继承产生的虚表指针占的字节
  • 因字节补齐多出来的字节

11.虚指针放在类末尾可以兼容C的结构体,而放在类开头则可以提高含有抽象基类、虚拟继承结构的效率

二、深入了解数据成员

1.编译器对类里面的函数成员进行分析其实是要在识别类的};之后才进行,注意参数列表不是这样,参数列表一声明就分析,即有可能会用到全局声明的类型

  • 目的是为了避免有一个全局变量和内部数据成员重名,而导致出现在定义数据成员语句前函数成员会去调用类外的全局变量
  • 现在很多将内联函数声明写在类内,定义写在类外的习惯在一定程度也是受老版本C++会先分析函数而出现以上问题的影响

2.目前大部分编译器对数据成员的布局(实际上C++标准允许任意顺序存放)

  • 按声明顺序连续存放
  • static忽略,因为它放在静态存储区
  • vptr放在所有显示声明的数据成员末尾,也有一些放在对象开头
  • 操作过程都是把一个以上的access sections连锁在一起,依照声明顺序成为一个连续区块。注意在1个section中声明8个变量和在8个section中总共声明8个变量对象大小是一样的

3.一段用于判断两个数据成员的先后地址关系代码,用到class member指针,值得深究

1
2
3
4
5
6
7
8
template<class class_type, class data_type1, class data_type2>
char* box(data_type1 class_type::*mem1, data_type1 class_type::*mem1){
return mem1 < mem2 ? "mem1 occure first" : "mem2 occure first";
}

box(&Point3d::z, &Point3d::y); //z和y是类Point3d中的float成员

//class_type会被绑定为Point3d,而data_type1和data_type2会被绑定为float

4.对象的直接调用成员和对象指针调用成员差异

1
2
3
4
Point3d origin, *pt = &origin;

origin.x = 0.0;
pt->x = 0.0;
  • 使用origin.x在编译期可以明确知道origin是Point3d类型,也知道具体地址
  • 而pt->x编译器会不明确pt是具体指向哪个class type,也许是虚基类呢,所以要等到执行期时经由一个间接导引才能够解决

5.无论继承还是被继承,static成员还是只有一个实例,它被提出到类外视作global,但注意它的只在类的生命周期内存活

  • 两个类都声明了一个同名的static成员,那么编译器暗中会对每一个static成员编码,每种编译器的暗中编码方法不同
  • static成员重名的话,会优先调用自己的static成员
  • 注意一点,指针是什么类型的它就会去调用什么类的静态成员,即父类指针指向子类对象,如果static成员同名也会使用父类指向的static,
  • 还有,如果多重继承的两个父类有重名static时,用子类对象、指针、作用域去调用该成员,就会报错ambiguous(有歧义)
  • static函数同上

6.对一个对象的成员用地址直接操作,由此可知一个足够勤劳的编译器在编译期就已经可以解决对y的存取了

1
2
3
4
5
6
7
Point3d origin;

//&origin.y = &origin + (&Point3d::y-1)
//即origin的y成员的地址等于origin对象地址+y在Point3d中的偏移量-1
//注意这个-1是编译系统为区分指向数据成员的指针是否有真实指向一个成员的所做的标识
//即Point3d::y的偏移值实际上是4,但因为C++有如上规定,所以&Point3d::y传回来的就是5,因此要-1
//注意某些编译器已经自动处理了这个,所以某些环境不用-1

7.类分层时要注意这样的占用空间

  • 不分层时
    1
    2
    3
    4
    5
    6
    7
    class A{
    int val;
    char bit1;
    char bit2;
    char bit3;
    };
    //总共占4+1+1+1+1(padding)=8字节
  • 分成3层后
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class A{
    int val;
    char bit1;
    };
    class B:public A{
    char bit2;
    };
    class C:public B{
    char bit3;
    };

    //一个C类对象占(4+1+3(padding))+(1+3(padding))+(1+3(padding))=16字节
  • C++这样做的原理是为保证使用父类指针指向不同子类时,每次父类指针的相互复制只改变父类部分,而不改变子类独有的成员
    1
    2
    3
    4
    5
    child c1;
    child c2;
    father *p1 = &c1;
    father *p2 = &c2;
    *p2 = *p1; //只改变p2的父类数据成员,对p2的子类独有成员无影响
  • 而如果不这样在继承体系中保持每个类的独立性,反而能把B类、C类的char成员插入到A类原本被padding占的空间中,在使用指针这样地址覆盖的形式*p2 = *p1,会导致*p2的子类独有成员也被篡改

8.3d类在2d类+=方法上的扩展模板

1
2
3
4
5
//此前2d类的+=要声明为虚函数
void operator+=(const Point2d& rhs){
Point2d::operator+={rhs}; //隐含this的Point2d部分 += rhs
z += rhs.z(); //隐含this->z += rhs.z()
}

9.多重继承的地址顺序

  • 先对继承的父类成员从左到右初始化
  • 再初始化自己的成员
  • 例子
    • 结构
    • 实例化C后地址

10.多重继承里加入虚拟继承后的地址

  • 分不变区域和共享区域
  • 不变区域中的数据成员不管后续如何衍化,都是拥有固定的offset偏移值(从实例化的对象开头算起)
  • 共享区域就是放虚拟继承的类,其位置会因为每次派生操作而有所变化,所以不能直接确定地址,只能间接存取,不同编译器有不同策略。
    • 现在一般还是每个对象对每一个虚拟父类背负一个额外的指针
      • 微软提出的解决办法是引入虚基类表,只有一个对象有至少一个虚拟父类就会给它安排一个指向虚基类表的指针(虚基类表中存指向各个虚拟父类的指针)
      • 另一个解决办法是在虚函数表中放虚基类的偏移值

11.因此一般而言,虚基类不应该含任何数据成员,否则会增加复杂性和开销

  • 即使这样,虚拟继承的引入也会增大开销,双层虚拟继承开销大于单层虚拟继承,以此类推

12.用偏移量直接取数据成员的办法,有些编译器会产生很大时间开销尽量不用,了解原理即可

1
2
3
father f1;
int father::*p1 = &father::a;
int temple = f1.*p1; //此时temple值其实就等于f1.a

三、深入了解函数成员

1.C++的设计准则之一就是成员函数至少要有和非成员函数一样的效率,即以下实现的两种方法效率在编译器中是一样的(实际上也是一样的,因为编译器在内部会把成员函数转换成非成员函数形式)

  • 非成员函数,通过传入类对象指针作为参数进行计算
  • 成员函数,直接调取类数据成员计算(但编译器内部也会把它换成用this指针的形式)

2.不管是类的数据成员还是函数成员,编译器在内部都会通过name mangling将其转换成独一无二的命名,例如Bar类的ival就有可能变成ival_3Bar。经过改进现在这个mangled名字一般不会出现在报错里但内部其实还是这种形式

3.对一个虚函数的调用

  • 如果normalize()是一个虚函数
  • 那么ptr->normalize()就会被内部转换成(*ptr->vptr[1])(ptr)的形式
    • vptr是由编译器产生的指针,它指向virtual table,它存在于包含虚函数成员的类对象中
    • [1]是virtual table slot的索引值,用来关联normalize()函数
    • (ptr)的ptr表示this指针
  • 由此可知如果显式地调用(类名::虚函数名),可以压制虚拟机制产生的重复操作,所以明确的时候(已经在某个类函数成员里再对某个虚函数进行调用,那么这个虚函数一定是此时这个类的函数)应尽量用显式调用
    • 隐式调用就是直接写函数名,不加类名::,这样会多一步对vptr的导向

4.一个返回int的静态成员函数的地址在编译器内部其实是unsigned int (*)()类型

  • 而不是unsigned int (类名*)()
  • 即把它当做nonmember函数指针

5.每一个类有一个虚函数表,每一个对象有一个虚表指针vptr指向虚函数表

  • 举一个直观的例子:
  • 一个父类指针p1指向一个子类对象,然后去调用父类和子类都有的box()
  • 按照指针类型此时应该只在父类里面去找box()的实现
  • 但是如果box()在父类里面被声明是虚函数的话,p1->box()会因为动态绑定的类型是子类而被vptr接手重新导向虚函数表,然后由虚函数表含有的虚函数地址指向子类的box()方法

6.具有多态性质的类对象(有虚函数),在执行期需要额外信息,编译器实现如下

  • 每个类有一个虚函数表,每个虚函数有一个表格索引值
  • 每个对象多一个字符串或数字成员,表示class类型
  • 每个对象多一个指针,指向某表格,表格中持有程序的虚函数执行期地址(这些地址在编译时已经确定好的,执行期不需要操作)

7.C++ primer的作者Stan在深度探索C++对象模型中明确指出,虚基类不要声明非静态数据成员,因为这除了增加编译底层的复杂性,并没有带来实际好处,反而会陷入地址offsets的迷宫

8.指向成员函数的指针

  • 定义
    1
    2
    double (Point::*coord)() = &Point::x;
    //表示定义一个名为coord的函数指针,它指向的是返回double类型的Point类里的x方法
  • 用指针形式使用该成员函数
    1
    2
    (对象名.*coord)(); //编译器内部会翻译成(coord)(&对象名)
    (对象指针->*coord)(); //编译器内部会翻译成(coord)(ptr)
  • 实际指向的地址是该成员函数在内存中的真正地址,但是这个值是不完全的,它需要被绑定在某个已经实例化的对象地址上才能通过这个地址来调用该函数
  • 所有的非虚函数成员函数都需要对象的地址(以参数this隐含指出),来存取实际的对象数据成员

9.虚函数表放在可执行文件的只读数据段

目录

一、基本语法

二、对象与类

三、接口、lambda表达式与内部类

四、异常、断言和日志

五、泛型程序设计

六、集合

七、并发

一、基本语法

1.Java区分大小写

2.类是构建所有Java应用程序和applet的构建块

3.一个简单的Java类

1
2
3
4
5
public class 类名{
public static void main(String[] args){
...
}
}

4.Java文件说明

  • 源代码文件名为为类名.java
  • 编译源代码后会得到类名.class
  • 然后就可以用控制台命令java 类名来运行代码了

5.打印输出

  • System.out.print();打印完后不会自动换行
  • System.out.println();打印完后会自动换行

6.Java数据类型

  • 整型(默认值0)
    • int占4字节
    • short占2字节
    • long占8字节
    • byte占1字节 -128-127
  • 浮点类型(默认值0)
    • float占4字节
    • double占8字节
  • 布尔类型(默认值false)(布尔类型不能与整型互换,即false!=0)
  • 字符、对象(默认值null)

7.前缀ob和OB都代表的是二进制,例如ob1111_0100(下划线是为了方便阅读,实际编译时会自动跳过)

8.值为NaN表示出错,此时用Double.isNaN(a)才能判断double类型的变量a是不是为NaN

  • 注意即使a=NaN、b=NaN,a也不等于b

9.若可以从变量的初始值推断出它的类型则可以直接用var定义

1
var i = 12;

10.枚举

  • 定义枚举类型
    1
    enum Size{SMALL, LARGE};
  • 使用
    1
    2

    Size s = Size.LARGE;

11.math包的导入形式

1
import static java.lang.Math.-;

12.强转(int)x会把x强制转换成整型,注意不要对boolean进行强制转换,最好用b?1:0这样的形式

13.a+=b+=c等同于a+=(b+=c)

14.s.substring(a,b)表示从字符串s取下标[a,b)的子串

15.将一个字符串与一个非字符串的值进行拼接时,后者会自动转换成字符串(任何一个Java对象都可以转换成字符串)

26.”ab”.repeat(3) = “ababab”;

27.字符串相等不要用==判断

  • 用s.eauals(t)判断s与t字符串是否相等
  • 用s.eaualsIgnoreCase(t)也是判断s与t字符串是否相等,但不区分大小写

28.要想通过控制台输入,首先需要构造一个Scanner对象

1
2
3
4
5
import java.util.-;

Scanner in = new Scanner(Systen.in);
s = in.next; //读一个以空格为分隔符的词
s = in.nextLine(); //读一行

29.以下语句会打印出1个空格和7个x里的字符(保留小数点后2位)

1
System.out.printf("%8.2f",x)

30.Java读写文件

  • 读取文件
    1
    2
    3
    4

    import java.util.-;

    Scanner in = new Scanner(Path.of("xx.txt"), StandardCharsets.UTF_8);
  • 写入文件
    1
    PrintWriter in = new PrintWriter(("xx.txt"), StandardCharsets.UTF_8);

31.throw Exception关键字用于把该函数的异常抛出到调用该函数的地方

32.带标签的break

1
2
3
4
5
6
7
...
read_data;
while{
for{
break read_data; //会跳过离read_data最近的循环,即while
}
}

33.声明数组

1
int[] a = new int[100]; 

34.循环的写法

1
2
3
for(int element:a){
...
}

35.数组拷贝

1
int[] b = Array.copyof(a, b数组长度); //若a长度小于b,则b的额外元素会取默认值

36.排序函数

1
Array.sort(a);

37.数组的使用方式

  • 数组的长度
    1
    int n = nums.length;
  • 在返回中构建
    1
    2
    3
    4
    5
    6

    # 返回一个包含元素0和1的长度为2的int[]
    return new int[] {0,1};

    # 返回一个空的int[]
    return new int[];

38.命令行运行.java代码

  • 先用javac生成.class文件
    1
    javac HelloWorld.java
  • 再用java执行
    1
    java HelloWorld

39.源文件名必须和类名相同

40.C++、Go等编译型与Java的区别

  • C++和Go最终得到的是.exe可执行程序,操作系统直接运行即可
  • Java最终得到的是.class字节码程序,操作系统运行的时候还需要Java解释器来解释才能执行

二、对象与类

1.Java类之间的关系

  • 依赖:该类的方法使用了另一个类的对象
  • 聚类:该类的对象包含一个别的类的对象
  • 继承:该类是别的类的扩展

2.一个程序会定义多个类,但其中只能一个类有main方法

  • main方法一般存在于用public声明的公共类里
  • 该公共类名字与源文件名字相同

3.静态方法是不对对象自身成员执行的方法,例如Math.pow(x,a)只对x、a执行,不对Math类里的数据成员执行,即可以不创建类对象也能调用静态方法(和C++一样)

4.方法的签名:方法名和参数类型

5.Java允许用package把类组织在一个集合中

6.一个包里的类可以使用

  • 该包的所有类
  • 其他同样被程序引入的包的公共类

7.引入包的格式

1
import java.xx.- //表示引入名为xx的包

8.通过jar在控制台启动代码

1
java -jar xx.jar

9.以下表示Manager是Employee的子类

1
public class Manager extends Employee

10.在子类中调用超类的方法用super关键字

1
2
3
class child{
super.getSalary(); //表示调用child的超类的getSalary()方法
}

11.子类构造器格式:

1
2
3
4
public 子类名(参数....){
super(参数1...);
子类自己的参数=xx;
}

12.可以指示多种实际类型的变量称多态变量,能根据对象类型选择不同方法的称作动态绑定

13.用final关键字声明的类和方法不允许继承

14.抽象类声明要用abstract关键字,注意该类不能创建对象

15.Object类是Java中所有类的超类,它意味着所有对象都可以用.equals()、.getclass()和toString()判断是否相同、返回其超类和返回”类名[字段值]”格式的字符串

16.ArrayList泛型数组在添加或删除元素时,它能够自动调整数组容量

  • 例:以下语句表示定义一个名为astaff的Employee数组
    1
    var staff = new ArrayList<Employee>(); //var相当于用来声明局部变量的auto
  • staff.add()-添加元素
  • staff.size()-返回元素个数
  • staff.trimToSize()-删掉没用到的空间,仅保存当前有数据的空间
  • staff.set(i, xx)-将第i个元素设为xx
  • staff.get(i)-返回第i个元素
  • staff.remove(i)-删除第i个元素

17.反射:在代码时,能够获取类内部的成员、方法等信息的程序称为反射

  • 例如所有类都有的方法
    • .getFields()-返回类的公共字段
    • .getMethods()-返回类的方法
    • .getConstructors()-返回类的构造器

18.Java和C++的区别是

  • Java的类最后括号不用加”;”
  • Java的构造函数的声明不能加返回值类型(c++会用void)
  • Java的main函数要用如下形式:
    1
    2
    3
    4

    public static void main(String[] args){
    ......
    }

19.每个.java文件里面

  • 一个源文件中只能有一个 public 类
  • 一个源文件可以有多个非 public 类
  • 源文件的名称应该和 public 类的类名保持一致。例如:源文件中 public 类的类名是 Employee,那么源文件应该命名为Employee.java。
  • 如果一个类定义在某个包中,那么 package 语句应该在源文件的首行。
  • 如果源文件包含 import 语句,那么应该放在 package 语句和类定义之间。如果没有 package 语句,那么 import 语句应该在源文件中最前面。
  • import 语句和 package 语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。

三、接口、lambda表达式与内部类

1.接口是对类的一组需求,满足了这些需求的类才称作符合这个接口,接口中会有方法的参数类型,返回值类型,但绝对不会实现方法

  • 接口声明用interface
    1
    2
    3
    4
    # 一个名为Comparable接口的声明
    public interface Comparable{
    int compare(Object other);
    }
    1
    2
    3
    4
    5
    6
    # Employee类实现了Comparable接口
    class Employee implements Comparable<Employee>{
    pbulic int compareTo(Employee ohter){
    return Double.compare(salart, other.salary);
    }
    }

四、异常、断言和日志

五、泛型程序设计

六、集合

七、并发

八、常用数据结构

1.哈希表

  • 声明
    1
    Map<Integer, Integer> memo = new HashMap<Integer, Integer>();
  • 插入
    1
    memo.put(key, val)
  • 检查是否存在
    1
    2

    if(memo.containsKey(target - nums[i])) return xx;
  • 获得val
    1
    int a = memo.get(key);

目录

概念

基础语法

并发编程

项目相关

报错案例

实战项目

概念

1.Go语言最主要的特性

  • 自动垃圾回收
  • 更丰富的内置类型
  • 函数多返回值
  • 错误处理
  • 匿名函数和闭包
  • 类型和接口
  • 并发编程
  • 反射
  • 语言交互性
阅读全文 »

1.int、turple、string类型在传值时默认是不可变的,其他统统按传引用处理

2.np.zeros((3,2), dtype=float)生成的是数组而不是list

  • 所以全局list变量要想在函数里赋值应该用如下形式,即事先加一个global声明
    1
    2
    3
    4
    5
    temple = []

    def xx():
    global temple
    temple = np.zeros((3,2), dtype=float)
阅读全文 »