Content
Introduction to ADTs in C ADT Initialization and Representation Invariants Interfaces and Implementations Abstraction Layers Testing an ADT Testing with stringstreams <- Go BackUniversity of Michigan at Ann Arbor
Last Edit Date: 02/06/2023
Disclaimer and Term of Use:
We do not guarantee the accuracy and completeness of the summary content. Some of the course material may not be included, and some of the content in the summary may not be correct. You should use this file properly and legally. We are not responsible for any results from using this file
This personal note is adapted from Professor Amir Kamil, Andrew DeOrio, James Juett, Sofia Saleem, and Saquib Razak. Please contact us to delete this file if you think your rights have been violated.
This work is licensed under a Creative Commons Attribution 4.0 International License.
The concept of abstraction can be applied to data as well. An abstract data type (ADT) separates the interface of a data type from its implementation, and it encompasses both the data itself as well as functionality on the data. An example of an ADT is the string
type in C++, used in the following code:
1 string str1 = "hello"; 2 string str2 = "jello"; 3 cout << str1 << endl; 4 if (str1.length() == str2.length()) { 5 cout << "Same length!" << endl; 6 }
A string
is an example of a full-featured C++ ADT, providing customized initialization, overloaded operations such as the stream-insertion operator, member functions, and so on.
The C language only has support for structs with data members (i.e. member variables). While this is sufficient to represent the data of an ADT, the functions that operate on the ADT must be defined separately from the struct. The following is the data definition of an ADT to represent triangles:
1 struct Triangle { 2 double a; 3 double b; 4 double c; 5 }; 6 7 int main() { 8 Triangle t1 = { 3, 4, 5 }; 9 Triangle t2 = { 2, 2, 2 }; 10 }
Try it out:
The Triangle
struct contains three member variables, one for each side of the triangle, each represented by a double
. The example in main()
creates and initializes two Triangle
structs, resulting in the following memory layout:
An ADT also includes functions that operate on the data. We can define functions to compute the perimeter of a triangle or to modify it by scaling each of the sides by a given factor:
1 // REQUIRES: tri points to a valid Triangle 2 // EFFECTS: Returns the perimeter of the given Triangle. 3 double Triangle_perimeter(const Triangle *tri) { 4 return tri->a + tri->b + tri->c; 5 } 6 7 // REQUIRES: tri points to a valid Triangle; s > 0 8 // MODIFIES: *tri 9 // EFFECTS: Scales the sides of the Triangle by the factor s. 10 void Triangle_scale(Triangle *tri, double s) { 11 tri->a *= s; 12 tri->b *= s; 13 tri->c *= s; 14 }
Our naming convention for functions that are part of a C-style ADT is to prepend the function name with the name of the ADT, Triangle
in this case. The first parameter is a pointer to the actual Triangle object the function works on. If the object need not be modified, we declare the pointer as a pointer to const
.
The following demonstrates how to use the Triangle
ADT functions:
1 int main() { 2 Triangle t1 = { 3, 4, 5 }; 3 Triangle_scale(&t1, 2); // sides are now 6, 8, 10 4 cout << Triangle_perimeter(&t1) << endl; // prints 24 5 }
Try it out
The code creates a Triangle
as a local variable and initializes it with sides 3, 4, and 5. It then scales the sides by a factor of 2 by calling Triangle_scale()
. Since that function takes a pointer to the actual triangle, we use the address-of operator to obtain and pass the address of t1
, as shown below
An essential component of proper ADT design is the use of representation invariants to express conditions that differentiate valid data (e.g. a Triangle with sides 3, 4, and 5) from invalid, nonsense values (e.g. one of the side lengths is -10).
Example 1
1 const int MAX_MATRIX_WIDTH = 500; 2 const int MAX_MATRIX_HEIGHT = 500; 3 4 struct Matrix{ 5 int width; 6 int height; 7 int data[MAX_MATRIX_WIDTH * MAX_MATRIX_HEIGHT]; 8 };
Some possible representation invariants:
0 < width <= MAX_MATRIX_WIDTH
0 < height <= MAX_MATRIX_HEIGHT
The first width * height
elements of data
are in valid information, the rest is junk
Elements are laid out in row-major order
Example 2
1 const int MAX_INTENSITY = 255; 2 3 struct Image { 4 int width; 5 int height; 6 Matrix red_channel; 7 Matrix green_channel; 8 Matrix blue_channel; 9 }
Some possible representation invariants:
0 < width <= MAX_MATRIX_WIDTH
0 < height <= MAX_MATRIX_HEIGHT
0 <= values stored in channels <= 255
width
/ height
members of all channel matrices are the same as the width
/ height
of image
We usually use assert()
to test these representation invariants.
We adhere to the convention of only interacting with an ADT through its interface. Usually, this means that we do not access the data members of an ADT in outside code.
However, occasionally we have the need for an ADT that provides no more functionality than grouping its members together.
Such an ADT is just plain old data (POD), without any functions that operate on that data, and we define its interface to be the same as its implementation.
The following is an example of a Pixel
struct used as a POD:
1 struct Pixel { 2 int r; // red 3 int g; // green 4 int b; // blue 5 }; 6 7 int main() { 8 Pixel p = { 255, 0, 0 }; 9 cout << p.r << " " << p.g << " " << p.b << endl; 10 }
The Pixel
ADT consists of just a data representation with no further functionality. Since it is a POD, its interface and implementation are the same, so it is acceptable to access its members directly.
As with procedural abstraction, data abstraction is also defined in layers, with each layer interacting solely with the interface of the layer below and not its implementation.
For example, we can represent an image using three matrices, one for each color channel.
Any code that uses an image relies on the image interface, without needing to know that it is implemented over three matrices.
Each matrix in turn can be represented using a single-dimensional array. Code that uses a matrix relies on the 2D abstraction provided by the interface without needing to know that it is implemented as a 1D array under the hood.